diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..d3490f4f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +# The service image builds with the repository root as its context (see +# docker/Dockerfile). Keep the context to what a build can use. +.git +.github +docs +**/dist +**/*.log +go.work +go.work.sum +smelt/snapshots diff --git a/.github/scripts/check-replaces.sh b/.github/scripts/check-replaces.sh new file mode 100755 index 00000000..0f47f2fa --- /dev/null +++ b/.github/scripts/check-replaces.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Every github.com/fil-forge/forge/* module that an in-repo go.mod requires must +# be resolved by a replace directive pointing at the sibling directory that +# holds it. Without one, Go resolves the path through the module proxy — which +# finds the repository root, not a module — and the failure is either +# confusing or, worse, absent. This check runs before anything else in CI. +set -euo pipefail + +fail=0 +while IFS= read -r gm; do + dir=$(dirname "$gm") + json=$(cd "$dir" && go mod edit -json) + for req in $(jq -r '.Require[]?.Path | select(startswith("github.com/fil-forge/forge/"))' <<<"$json"); do + target=$(jq -r --arg p "$req" '.Replace[]? | select(.Old.Path == $p) | .New.Path' <<<"$json") + if [ -z "$target" ]; then + echo "$gm: requires $req with no replace directive" + fail=1 + continue + fi + if [ ! -f "$dir/$target/go.mod" ]; then + echo "$gm: replace for $req points at $target, which has no go.mod" + fail=1 + continue + fi + got=$(cd "$dir/$target" && go mod edit -json | jq -r .Module.Path) + if [ "$got" != "$req" ]; then + echo "$gm: $target is module $got, not $req" + fail=1 + fi + done +done < <(git ls-files '*/go.mod') + +if [ "$fail" = 0 ]; then + echo "every in-repo require has a matching replace" +fi +exit "$fail" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d54c207..86f89910 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,23 @@ name: CI -# Two tiers, mirroring the old per-repo split but now over one commit: +# Two tiers over one commit: # -# unit per-module, path-filtered, GOWORK=off — each service is still an -# independent module and is tested as one, in seconds. +# unit per-module, GOWORK=off — every module in the repo is built, vetted, +# tidy-checked and tested as the standalone module it is, in minutes. # stack ONE full-stack run per PR, at the commit under test, with every -# in-repo service built from the working tree and mounted over the -# published images. +# in-repo service image built from the working tree and handed to the +# suites through the runner's Docker daemon — never pushed. # -# The stack tier is the whole point of the monorepo. It resolves no binary for -# an in-repo service from a registry image, so there is no floating `:main` -# baseline to go stale and no lockfile to promote — "the stack" is just this -# commit. +# Every module runs on every change. The unit tier used to be path-filtered +# per service, which was sound while no service module imported another and +# the shared surface lived in an external library. It stopped being sound +# when the shared modules (protocol/, internal/, attestation/) and the +# service-to-service dependencies (ingot -> the indexer's query types, +# hilt -> smelt's stack harness) moved +# in-repo: a service's behaviour now depends on directories outside its own +# subtree, and a path filter that misses one skips CI silently and reports +# green. The whole matrix costs minutes of runner time; a silent skip costs a +# broken main. on: pull_request: @@ -27,75 +33,66 @@ concurrency: cancel-in-progress: true jobs: - # Which services does this diff actually touch? No service module imports - # another (shared surface lives in libforge), so each filter is just the - # service's own subtree. - changes: - name: detect changed services + # Every in-repo module resolves its siblings through replace directives in + # its own go.mod — the workspace is a developer convenience that CI never + # uses. A require without a replace resolves through the module proxy to + # whatever lives at that path, which for github.com/fil-forge/forge/... is + # the repository root: not a module, and the error is either confusing or + # absent. This job makes that state impossible to merge. + replaces: + name: replace directives · every in-repo require has one runs-on: ubuntu-latest - outputs: - services: ${{ steps.filter.outputs.changes }} - any: ${{ steps.filter.outputs.changes != '[]' }} steps: - uses: actions/checkout@v5 - - uses: dorny/paths-filter@v3 - id: filter + - uses: actions/setup-go@v6 with: - filters: | - piri: - - 'piri/**' - - 'docker/**' - - '.github/workflows/ci.yml' - hilt: - - 'hilt/**' - - 'docker/**' - - '.github/workflows/ci.yml' - sprue: - - 'sprue/**' - - 'docker/**' - - '.github/workflows/ci.yml' - ingot: - - 'ingot/**' - - 'docker/**' - - '.github/workflows/ci.yml' - smelt: - - 'smelt/**' - - 'docker/**' - - '.github/workflows/ci.yml' + go-version-file: protocol/go.mod + cache: false + - run: .github/scripts/check-replaces.sh unit: # The check name spells out the gate order so a red X is legible from the # PR checks list without clicking through. - name: unit ${{ matrix.service }} · build,vet,tidy,test - needs: changes - if: needs.changes.outputs.any == 'true' + name: unit ${{ matrix.module }} · build,vet,tidy,test runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 25 strategy: fail-fast: false matrix: - service: ${{ fromJSON(needs.changes.outputs.services) }} + module: + - protocol + - internal + - attestation + - piri + - hilt + - sprue + - ingot + - smelt + - smelt/systems/stress-tester + - delegator + - piri-signing-service + - indexing-service defaults: run: - working-directory: ${{ matrix.service }} + working-directory: ${{ matrix.module }} env: - # Each service is a standalone module and is tested as one. The - # workspace is a developer convenience; go.mod is the source of truth, - # and this proves it is sufficient. + # Each module is standalone and is tested as one. The workspace is a + # developer convenience; go.mod (with its replace directives) is the + # source of truth, and this proves it is sufficient. GOWORK: off CGO_ENABLED: 0 steps: - uses: actions/checkout@v5 - uses: actions/setup-go@v6 with: - go-version-file: ${{ matrix.service }}/go.mod - cache-dependency-path: ${{ matrix.service }}/go.sum + go-version-file: ${{ matrix.module }}/go.mod + cache-dependency-path: ${{ matrix.module }}/go.sum # piri builds CGO-free only with `skiff`, which selects Curio's FFI-free # variants; without it the build fails on undefined ffi.* symbols. - name: Set build flags run: | - if [ "${{ matrix.service }}" = "piri" ]; then + if [ "${{ matrix.module }}" = "piri" ]; then echo "GOFLAGS=-tags=skiff" >> "$GITHUB_ENV" fi @@ -104,33 +101,39 @@ jobs: - name: go vet run: go vet ./... + # The wire contract's codecs are generated (cbor-gen + dag-json-gen). A + # type edited without regenerating is a silent wire change; a generator + # that no longer compiles goes unnoticed until someone runs go generate. + - name: codegen gate + if: matrix.module == 'protocol' || matrix.module == 'attestation' + run: make codegen-build gen-check + # Placeholder gate: no repo-wide linter rules are adopted yet. When they - # are, they live in ONE root-level config shared by every service — do - # not add per-service lint configs. + # are, they live in ONE root-level config shared by every module — do + # not add per-module lint configs. - name: lint run: echo "lint | no repo-wide linter rules adopted yet" # A module's go.mod/go.sum must be exactly what tidy produces — drift - # here is how version skew between the services creeps back in. + # here is how version skew between the modules creeps back in. - name: go.mod tidy check run: go mod tidy && git diff --exit-code go.mod go.sum - name: go test run: go test ./... - # One stack run for the whole PR, not one per service. The four in-repo - # service images are built HERE, from this commit, and handed to the suites - # through the runner's Docker daemon — never pushed to a registry. The - # artifact under test is the artifact that ships, packaging included; there - # is no binary-mounting layer in CI (that stays a local-dev fast path). + # One stack run for the whole PR, not one per service. Every in-repo service + # image is built HERE, from this commit, and handed to the suites through + # the runner's Docker daemon — never pushed to a registry. The artifact + # under test is the artifact that ships, packaging included; there is no + # binary-mounting layer in CI (that stays a local-dev fast path). stack: name: stack · images,e2e,s3-suite - needs: [changes, unit] - if: needs.changes.outputs.any == 'true' + needs: [replaces, unit] # Pinned rather than -latest so the Docker version doesn't drift from under # us. The runner's preinstalled Docker is 28.x, well past smelt's 25+ floor. runs-on: ubuntu-24.04 - timeout-minutes: 50 + timeout-minutes: 60 env: GOWORK: off # The images built below, resolved by smelt's compose env plumbing. @@ -138,6 +141,9 @@ jobs: HILT_IMAGE: forge-ci/hilt:${{ github.sha }} INGOT_IMAGE: forge-ci/ingot:${{ github.sha }} UPLOAD_IMAGE: forge-ci/sprue:${{ github.sha }} + DELEGATOR_IMAGE: forge-ci/delegator:${{ github.sha }} + SIGNER_IMAGE: forge-ci/piri-signing-service:${{ github.sha }} + INDEXER_IMAGE: forge-ci/indexing-service:${{ github.sha }} # Tells the S3 suite the images above ARE this commit — run them as # built, no binary mounts (see forgeStack in smelt/tests/s3). SMELT_STACK_PREBUILT: '1' @@ -155,13 +161,16 @@ jobs: - uses: docker/setup-buildx-action@v3 - # All four images at this commit, loaded into the local daemon. The - # cache scopes are shared with publish-ghcr.yml, so merges to main keep - # these warm and an unchanged service's build is a near no-op. + # Every service image at this commit, loaded into the local daemon. The + # build context is the repository root (the services reach the shared + # modules through replace directives, so a per-service context cannot + # see them). The cache scopes are shared with publish-ghcr.yml, so + # merges to main keep these warm and an unchanged service's build is a + # near no-op. - name: Build piri image uses: docker/build-push-action@v6 with: - context: piri + context: . file: docker/Dockerfile target: prod build-args: | @@ -175,7 +184,7 @@ jobs: - name: Build hilt image uses: docker/build-push-action@v6 with: - context: hilt + context: . file: docker/Dockerfile target: prod build-args: | @@ -188,7 +197,7 @@ jobs: - name: Build sprue image uses: docker/build-push-action@v6 with: - context: sprue + context: . file: docker/Dockerfile target: prod build-args: | @@ -201,7 +210,7 @@ jobs: - name: Build ingot image uses: docker/build-push-action@v6 with: - context: ingot + context: . file: docker/Dockerfile target: prod build-args: | @@ -212,6 +221,52 @@ jobs: tags: forge-ci/ingot:${{ github.sha }} cache-from: type=gha,scope=ingot cache-to: type=gha,mode=max,scope=ingot + # The satellites keep the binary names their compose files and smelt's + # workspace table expect (BIN), which differ from their directory names. + - name: Build delegator image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + target: prod + build-args: | + SERVICE=delegator + MAIN_PKG=. + BIN=registrar + load: true + push: false + tags: forge-ci/delegator:${{ github.sha }} + cache-from: type=gha,scope=delegator + cache-to: type=gha,mode=max,scope=delegator + - name: Build piri-signing-service image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + target: prod + build-args: | + SERVICE=piri-signing-service + MAIN_PKG=. + BIN=signer + load: true + push: false + tags: forge-ci/piri-signing-service:${{ github.sha }} + cache-from: type=gha,scope=piri-signing-service + cache-to: type=gha,mode=max,scope=piri-signing-service + - name: Build indexing-service image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + target: prod + build-args: | + SERVICE=indexing-service + BIN=indexer + load: true + push: false + tags: forge-ci/indexing-service:${{ github.sha }} + cache-from: type=gha,scope=indexing-service + cache-to: type=gha,mode=max,scope=indexing-service # The suites run sequentially in this one job: each sweeps smeltery-* # containers, so two stacks must never share a Docker host. @@ -238,11 +293,11 @@ jobs: { echo "## Stack tier" echo - echo "All four service images are built from **this commit** (\`docker/Dockerfile\`), loaded into the runner's daemon — never pushed — and the suites boot the stack on them as-built." + echo "Every service image is built from **this commit** (\`docker/Dockerfile\`), loaded into the runner's daemon — never pushed — and the suites boot the stack on them as-built." echo echo "| image | target | tag |" echo "|---|---|---|" - for s in piri hilt sprue ingot; do + for s in piri hilt sprue ingot delegator piri-signing-service indexing-service; do echo "| $s | prod | \`forge-ci/$s:${GITHUB_SHA}\` |" done echo diff --git a/.github/workflows/publish-ghcr.yml b/.github/workflows/publish-ghcr.yml index 14471a2e..55d5cd04 100644 --- a/.github/workflows/publish-ghcr.yml +++ b/.github/workflows/publish-ghcr.yml @@ -9,13 +9,20 @@ name: Container # images from the PR's tree and boots the stack on them, so an unbuildable # image fails the PR anyway. # +# Every service publishes on every merge. This used to be path-filtered per +# service; that stopped being sound once the services shared in-repo modules +# and depended on one another (see ci.yml). With the gha build cache warm, an +# unchanged service's build is a near no-op, so the cost of always publishing +# is small and the cost of a stale image is not. +# # The dev images (:main-dev) are published artifacts for ad-hoc engineer # debugging (debug toolset + dlv; smelt's compose.debug.yml / make debug- # flow) — built here on merge, never on PRs. # # Images are ghcr.io/fil-forge/forge/, following the repo. The old # ghcr.io/fil-forge/ packages are left in place so existing pulls and -# already-published snapshots keep resolving. +# already-published snapshots keep resolving; the satellites' own repositories +# keep publishing theirs until they are retired. on: push: @@ -31,45 +38,21 @@ concurrency: cancel-in-progress: true jobs: - changes: - name: Detect changes - runs-on: ubuntu-latest - outputs: - services: ${{ steps.filter.outputs.changes }} - any: ${{ steps.filter.outputs.changes != '[]' }} - steps: - - uses: actions/checkout@v5 - - uses: dorny/paths-filter@v3 - id: filter - with: - filters: | - piri: - - 'piri/**' - - 'docker/**' - - '.github/workflows/publish-ghcr.yml' - hilt: - - 'hilt/**' - - 'docker/**' - - '.github/workflows/publish-ghcr.yml' - sprue: - - 'sprue/**' - - 'docker/**' - - '.github/workflows/publish-ghcr.yml' - ingot: - - 'ingot/**' - - 'docker/**' - - '.github/workflows/publish-ghcr.yml' - build: name: publish (${{ matrix.service }}) - needs: changes - if: needs.changes.outputs.any == 'true' runs-on: ubuntu-latest timeout-minutes: 60 strategy: fail-fast: false matrix: - service: ${{ fromJSON(needs.changes.outputs.services) }} + service: + - piri + - hilt + - sprue + - ingot + - delegator + - piri-signing-service + - indexing-service steps: - uses: actions/checkout@v5 @@ -91,21 +74,29 @@ jobs: type=raw,value=main type=sha,prefix=sha-,format=short - # Per-service parameters for the shared Dockerfile. + # Per-service parameters for the shared Dockerfile (see its header for + # what MAIN_PKG, BIN and BUILD_TAGS mean). - name: Service build args id: args run: | extra="" case "${{ matrix.service }}" in - piri) extra="BUILD_TAGS=skiff" ;; - ingot) extra="MAIN_PKG=./cmd/ingot" ;; + piri) extra="BUILD_TAGS=skiff" ;; + ingot) extra="MAIN_PKG=./cmd/ingot" ;; + delegator) extra=$'MAIN_PKG=.\nBIN=registrar' ;; + piri-signing-service) extra=$'MAIN_PKG=.\nBIN=signer' ;; + indexing-service) extra="BIN=indexer" ;; esac - echo "extra=$extra" >> "$GITHUB_OUTPUT" + { + echo "extra<> "$GITHUB_OUTPUT" - name: Build and push (prod) uses: docker/build-push-action@v6 with: - context: ${{ matrix.service }} + context: . file: docker/Dockerfile target: prod build-args: | @@ -121,7 +112,7 @@ jobs: - name: Build and push (dev) uses: docker/build-push-action@v6 with: - context: ${{ matrix.service }} + context: . file: docker/Dockerfile target: dev build-args: | diff --git a/Makefile b/Makefile index deb18dbd..024171ba 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ # forge — root Makefile. # -# Pure delegation: every recipe here runs `make -C` into a service; no build -# logic lives at the root. `cd && make ` always works standalone, -# and adding a service means adding it to a list below, nothing more. +# Pure delegation: every recipe here runs `make -C` into a module; no build +# logic lives at the root. `cd && make ` always works +# standalone, and adding a module means adding it to a list below, nothing +# more. # # The verb contract every service Makefile implements: # @@ -10,18 +11,24 @@ # test unit tests (fast, no Docker) image prod container via # clean remove build outputs docker/Dockerfile # +# The shared library modules (protocol, internal, attestation) implement +# build/vet/test only — they ship no binary and no image. protocol and +# attestation also carry the codegen gate (`make gen-check`). +# # smelt is the stack harness, not a service: it joins test/vet, and its own # `build`/`clean`/`up`/`down` are STACK lifecycle commands — drive those from # smelt/ directly. # Services: independent Go modules that ship a container. -SERVICES := piri hilt sprue ingot +SERVICES := piri hilt sprue ingot delegator piri-signing-service indexing-service +# Library modules shared by the services (no binary, no image). +LIBRARIES := protocol internal attestation # Everything with Go unit tests (smelt's units join test/vet). -GO_MODULES := $(SERVICES) smelt +GO_MODULES := $(LIBRARIES) $(SERVICES) smelt .DEFAULT_GOAL := help -.PHONY: build vet test images itest e2e clean help +.PHONY: build vet test gen-check check-replaces images itest e2e clean help ## build: compile every service binary build: @@ -35,6 +42,14 @@ vet: test: @set -e; for s in $(GO_MODULES); do $(MAKE) -C $$s test; done +## gen-check: regenerate the wire codecs and fail on any diff (protocol, attestation) +gen-check: + @set -e; for s in protocol attestation; do $(MAKE) -C $$s codegen-build gen-check; done + +## check-replaces: every in-repo require in every go.mod has a matching replace +check-replaces: + .github/scripts/check-replaces.sh + ## images: build every service's prod container from docker/Dockerfile images: @set -e; for s in $(SERVICES); do $(MAKE) -C $$s image; done @@ -51,8 +66,8 @@ e2e: clean: @set -e; for s in $(SERVICES); do $(MAKE) -C $$s clean; done -## /: run any target in any service, e.g. `make piri/test` -%/build %/vet %/test %/image %/image-dev %/clean %/gen %/itest: +## /: run any target in any module, e.g. `make piri/test` +%/build %/vet %/test %/image %/image-dev %/clean %/gen %/gen-check %/itest: $(MAKE) -C $(@D) $(@F) ## help: list root targets diff --git a/README.md b/README.md new file mode 100644 index 00000000..b96b8b35 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# forge + +The Forge network's services and the code they share, as one repository of +independent Go modules. + +## Layout + +| directory | module | what it is | +|---|---|---| +| `piri/` | `github.com/fil-forge/forge/piri` | storage node | +| `hilt/` | `…/forge/hilt` | tenancy, access keys and the S3 wire contract's authorizer | +| `sprue/` | `…/forge/sprue` | upload service | +| `ingot/` | `…/forge/ingot` | S3 gateway over the network | +| `delegator/` | `…/forge/delegator` | storage-provider onboarding and delegation issuance | +| `piri-signing-service/` | `…/forge/piri-signing-service` | EIP-712 signing oracle for PDP operations | +| `indexing-service/` | `…/forge/indexing-service` | content-routing index (IPNI + UCAN content claims) | +| `smelt/` | `…/forge/smelt` | the stack harness: boots the whole network in Docker; owns the e2e, S3 and compatibility suites | +| `protocol/` | `…/forge/protocol` | the wire contract: every UCAN command binding (`commands/**`), `blobindex`, `receipt`, `retrieval`, and the `bytemap`/`digestutil` helpers they need. Depends only on ucantone and third-party modules | +| `internal/` | `…/forge/internal` | helpers the services share that are not wire contract: `identity`, `jobqueue`, `piece`, `sigv4`, `s3perm`, `zapucan`, the `client/hilt` and `client/delegator` RPC clients, `pdpsigner`, `ipni/advertisement`. Importable only from this repository | +| `attestation/` | `…/forge/attestation` | the did:mailto attestation extension (`attestation`, `didmailto`, the `attest` command). Depends only on ucantone and multiformats | +| `docker/` | — | the one `Dockerfile` every service image builds from | +| `.github/` | — | CI (`ci.yml`), image publishing, releases, and the compatibility workflow | + +Every service is a standalone module and is built, tested and packaged as +one. Shared code is reached through `replace` directives in each service's +`go.mod` (`replace github.com/fil-forge/forge/protocol => ../protocol`, and +so on); the root `go.work` exists for editing across modules and is never used +by CI or by the image build. `.github/scripts/check-replaces.sh` verifies that +every in-repo `require` has its `replace`. + +## Building and testing + +```sh +make build # every service binary +make test # every module's unit tests (no Docker) +make vet +make gen-check # regenerate protocol/attestation codecs, fail on a diff +make check-replaces +make images # every service's prod image via docker/Dockerfile +make e2e # smelt's full-stack smoke suite (Docker) +make itest # the S3 gateway system suite (Docker) +make / # e.g. make piri/test +``` + +Inside a module, the standard loop is `GOWORK=off go build ./... && go vet +./... && go test ./...` — `GOWORK=off` is what CI uses, and what proves the +module's `go.mod` is sufficient on its own. piri needs `-tags skiff`. + +## Images + +```sh +docker build -f docker/Dockerfile --build-arg SERVICE= --target prod . +``` + +The build context is the repository root. `MAIN_PKG` selects the main +package (`./cmd` by default; `./cmd/ingot`, `.` for delegator and +piri-signing-service), `BIN` the binary's name inside the image where it +differs from the directory (`registrar`, `signer`, `indexer`), `BUILD_TAGS` +extra build tags. See the Dockerfile header. diff --git a/attestation/LICENSE.md b/attestation/LICENSE.md new file mode 100644 index 00000000..8b486090 --- /dev/null +++ b/attestation/LICENSE.md @@ -0,0 +1,232 @@ +The contents of this repository are Copyright (c) corresponding authors and +contributors, licensed under the `Permissive License Stack` meaning either of: + +- Apache-2.0 Software License: https://www.apache.org/licenses/LICENSE-2.0 + ([...4tr2kfsq](https://dweb.link/ipfs/bafkreiankqxazcae4onkp436wag2lj3ccso4nawxqkkfckd6cg4tr2kfsq)) + +- MIT Software License: https://opensource.org/licenses/MIT + ([...vljevcba](https://dweb.link/ipfs/bafkreiepofszg4gfe2gzuhojmksgemsub2h4uy2gewdnr35kswvljevcba)) + +You may not use the contents of this repository except in compliance +with one of the listed Licenses. For an extended clarification of the +intent behind the choice of Licensing please refer to +https://protocol.ai/blog/announcing-the-permissive-license-stack/ + +Unless required by applicable law or agreed to in writing, software +distributed under the terms listed in this notice is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See each License for the specific language +governing permissions and limitations under that License. + + + +`SPDX-License-Identifier: Apache-2.0 OR MIT` + +Verbatim copies of both licenses are included below: + +
Apache-2.0 Software License + +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +``` + +
+ +
MIT Software License + +``` +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` + +
diff --git a/attestation/Makefile b/attestation/Makefile new file mode 100644 index 00000000..f064c65f --- /dev/null +++ b/attestation/Makefile @@ -0,0 +1,26 @@ +.PHONY: gen build vet test codegen-build gen-check ci + +gen: + go generate ./... + +build: + go build ./... + +vet: + go vet ./... + +test: + go test ./... + +# Compile the generators under the tag they run with, so a generator that no +# longer compiles fails here rather than at the next `go generate`. +codegen-build: + go build -tags codegen -o $(shell mktemp -d) $(shell go list ./... | grep '/gen$$') + +# Regenerate and fail on any diff: catches stale generated files and generators +# that no longer compile. +gen-check: gen + @git diff --exit-code -- '*_gen.go' '*_gen.*.go' || \ + { echo "generated files are out of date; run 'make gen' and commit the result" >&2; exit 1; } + +ci: build vet codegen-build gen-check test diff --git a/attestation/attest/cbor_gen.go b/attestation/attest/cbor_gen.go new file mode 100644 index 00000000..6443879d --- /dev/null +++ b/attestation/attest/cbor_gen.go @@ -0,0 +1,184 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package attest + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *ProofArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Proof (cid.Cid) (struct) + if len("proof") > 8192 { + return xerrors.Errorf("Value in field \"proof\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("proof"))); err != nil { + return err + } + if _, err := cw.WriteString(string("proof")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Proof); err != nil { + return xerrors.Errorf("failed to write cid field t.Proof: %w", err) + } + + return nil +} + +func (t *ProofArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ProofArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ProofArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Proof (cid.Cid) (struct) + case "proof": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Proof: %w", err) + } + + t.Proof = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *Unit) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{160}); err != nil { + return err + } + return nil +} + +func (t *Unit) UnmarshalCBOR(r io.Reader) (err error) { + *t = Unit{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Unit: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 0) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/attestation/attest/gen/main.go b/attestation/attest/gen/main.go new file mode 100644 index 00000000..98e23f3e --- /dev/null +++ b/attestation/attest/gen/main.go @@ -0,0 +1,42 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/attestation/attest" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + attest.ProofArguments{}, + attest.Unit{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "attest", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "attest", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/attestation/attest/json_gen.go b/attestation/attest/json_gen.go new file mode 100644 index 00000000..21052d7a --- /dev/null +++ b/attestation/attest/json_gen.go @@ -0,0 +1,188 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package attest + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *ProofArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Proof (cid.Cid) (struct) + if len("proof") > 8192 { + return fmt.Errorf("string in field \"proof\" was too long") + } + if err := jw.WriteString(string("proof")); err != nil { + return fmt.Errorf("writing string for field \"proof\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Proof); err != nil { + return fmt.Errorf("writing CID for field t.Proof: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ProofArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ProofArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ProofArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ProofArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ProofArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ProofArguments: string too large") + } + return fmt.Errorf("reading string for field ProofArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ProofArguments: %w", err) + } + switch name { + + // t.Proof (cid.Cid) (struct) + case "proof": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Proof: %w", err) + } + t.Proof = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ProofArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ProofArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ProofArguments") + } + } + } + + return nil +} +func (t *Unit) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *Unit) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Unit{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for Unit: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for Unit: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for Unit: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field Unit: string too large") + } + return fmt.Errorf("reading string for field Unit: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field Unit: %w", err) + } + switch name { + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for Unit: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field Unit: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for Unit") + } + } + } + + return nil +} diff --git a/attestation/attest/proof.go b/attestation/attest/proof.go new file mode 100644 index 00000000..06343607 --- /dev/null +++ b/attestation/attest/proof.go @@ -0,0 +1,14 @@ +//go:build !codegen + +package attest + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type ProofOK = Unit + +// Issued by a trusted authority (usually the one handling invocation) that +// attests a specific UCAN delegation has been considered authentic. +var Proof = binding.Bind[*ProofArguments, *ProofOK](command.MustParse("/ucan/attest/proof")) diff --git a/attestation/attest/types.go b/attestation/attest/types.go new file mode 100644 index 00000000..e5193e86 --- /dev/null +++ b/attestation/attest/types.go @@ -0,0 +1,11 @@ +package attest + +import "github.com/ipfs/go-cid" + +type ProofArguments struct { + Proof cid.Cid `cborgen:"proof" dagjsongen:"proof"` +} + +// Unit is the empty wire type the attestation receipt carries: it encodes as +// an empty CBOR map / dag-json object. +type Unit struct{} diff --git a/attestation/didmailto/did.go b/attestation/didmailto/did.go new file mode 100644 index 00000000..41453e51 --- /dev/null +++ b/attestation/didmailto/did.go @@ -0,0 +1,63 @@ +package didmailto + +import ( + "fmt" + "net/mail" + "net/url" + "strings" + + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/errors" +) + +const InvalidMailtoDIDErrorName = "InvalidMailtoDID" + +var ErrInvalidMailtoDID = errors.New(InvalidMailtoDIDErrorName, "invalid mailto DID") + +// New creates a new mailto DID from an RFC 5322 formatted email address. +func New(email string) (did.DID, error) { + a, err := mail.ParseAddress(email) + if err != nil { + return did.DID{}, fmt.Errorf("parsing email: %w", err) + } + at := strings.LastIndex(a.Address, "@") + var local, domain string + if at < 0 { + // This is a malformed address ("@" is required in addr-spec); + return did.DID{}, fmt.Errorf("malformed email address: %s", email) + } + local, domain = a.Address[:at], a.Address[at+1:] + return did.New(Method, fmt.Sprintf("%s:%s", url.QueryEscape(domain), url.QueryEscape(local))), nil +} + +// Email extracts the email address from the DID. +func Email(d did.DID) (string, error) { + if !strings.HasPrefix(d.String(), "did:mailto:") { + return "", ErrInvalidMailtoDID + } + parts := strings.Split(d.String(), ":") + emailLocal, err := url.QueryUnescape(parts[3]) + if err != nil { + return "", ErrInvalidMailtoDID + } + domain, err := url.QueryUnescape(parts[2]) + if err != nil { + return "", ErrInvalidMailtoDID + } + return fmt.Sprintf("%s@%s", emailLocal, domain), nil +} + +func Parse(str string) (did.DID, error) { + d, err := did.Parse(str) + if err != nil { + return did.DID{}, err + } + if !strings.HasPrefix(d.String(), "did:mailto:") { + return did.DID{}, ErrInvalidMailtoDID + } + return d, nil +} + +func Format(d did.DID) string { + return d.String() +} diff --git a/attestation/didmailto/did_test.go b/attestation/didmailto/did_test.go new file mode 100644 index 00000000..1c77ca5f --- /dev/null +++ b/attestation/didmailto/did_test.go @@ -0,0 +1,141 @@ +package didmailto + +import ( + "testing" + + "github.com/fil-forge/ucantone/did" + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + t.Run("simple email", func(t *testing.T) { + d, err := New("user@example.com") + require.NoError(t, err) + require.Equal(t, "did:mailto:example.com:user", d.String()) + }) + + t.Run("subdomain", func(t *testing.T) { + d, err := New("user@mail.example.com") + require.NoError(t, err) + require.Equal(t, "did:mailto:mail.example.com:user", d.String()) + }) + + t.Run("missing @ returns error", func(t *testing.T) { + _, err := New("notanemail") + require.Error(t, err) + }) + + t.Run("multiple @ returns error", func(t *testing.T) { + _, err := New("a@b@c") + require.Error(t, err) + }) + + t.Run("empty string returns error", func(t *testing.T) { + _, err := New("") + require.Error(t, err) + }) +} + +func TestEmail(t *testing.T) { + t.Run("round-trips simple email", func(t *testing.T) { + d, err := New("user@example.com") + require.NoError(t, err) + email, err := Email(d) + require.NoError(t, err) + require.Equal(t, "user@example.com", email) + }) + + t.Run("round-trips local part with plus sign", func(t *testing.T) { + d, err := New("user+tag@example.com") + require.NoError(t, err) + email, err := Email(d) + require.NoError(t, err) + require.Equal(t, "user+tag@example.com", email) + }) + + t.Run("non-mailto DID returns error", func(t *testing.T) { + d, err := did.Parse("did:web:example.com") + require.NoError(t, err) + _, err = Email(d) + require.ErrorIs(t, err, ErrInvalidMailtoDID) + }) + + t.Run("round-trips unusual emails", func(t *testing.T) { + emails := []string{ + // https://gist.github.com/cjaoude/fd9910626629b53c4d25#file-gistfile1-txt-L5 + "email@example.com", + "firstname.lastname@example.com", + "email@subdomain.example.com", + "firstname+lastname@example.com", + "email@123.123.123.123", + "email@[123.123.123.123]", + // TODO: quoted addresses do not roundtrip with net/mail because the + // quotes are stripped out during parsing + // "\"email\"@example.com", + // "\"email@1\"@example.com", + "1234567890@example.com", + "email@example-one.com", + "_______@example.com", + "email@example.name", + "email@example.museum", + "email@example.co.jp", + "firstname-lastname@example.com", + // https://gist.github.com/cjaoude/fd9910626629b53c4d25#file-gistfile1-txt-L24 + // TODO: none of these parse with net/mail + // "much.”more\\ unusual”@example.com", + // "very.unusual.”@”.unusual.com@example.com", + // "very.”(),:;<>[]”.VERY.”very@\\ \"very”.unusual@strange.example.com", + } + for _, email := range emails { + t.Run(email, func(t *testing.T) { + d, err := New(email) + require.NoError(t, err) + t.Log(d.String()) + got, err := Email(d) + require.NoError(t, err) + require.Equal(t, email, got) + }) + } + }) +} + +func TestParse(t *testing.T) { + t.Run("valid mailto DID", func(t *testing.T) { + d, err := Parse("did:mailto:example.com:user") + require.NoError(t, err) + require.Equal(t, "did:mailto:example.com:user", d.String()) + }) + + t.Run("non-mailto DID returns error", func(t *testing.T) { + _, err := Parse("did:web:example.com") + require.ErrorIs(t, err, ErrInvalidMailtoDID) + }) + + t.Run("invalid DID string returns error", func(t *testing.T) { + _, err := Parse("not-a-did") + require.Error(t, err) + }) +} + +func TestFormat(t *testing.T) { + d, err := New("user@example.com") + require.NoError(t, err) + require.Equal(t, d.String(), Format(d)) +} + +func TestNewEmailRoundTrip(t *testing.T) { + emails := []string{ + "user@example.com", + "user+tag@example.com", + "first.last@sub.domain.org", + } + for _, email := range emails { + t.Run(email, func(t *testing.T) { + d, err := New(email) + require.NoError(t, err) + got, err := Email(d) + require.NoError(t, err) + require.Equal(t, email, got) + }) + } +} diff --git a/attestation/didmailto/didmailto.go b/attestation/didmailto/didmailto.go new file mode 100644 index 00000000..b7c3c4af --- /dev/null +++ b/attestation/didmailto/didmailto.go @@ -0,0 +1,3 @@ +package didmailto + +const Method = "mailto" diff --git a/attestation/didmailto/resolver.go b/attestation/didmailto/resolver.go new file mode 100644 index 00000000..fdd1e7aa --- /dev/null +++ b/attestation/didmailto/resolver.go @@ -0,0 +1,37 @@ +package didmailto + +import ( + "context" + + "github.com/fil-forge/forge/attestation" + "github.com/fil-forge/ucantone/did" +) + +func NewResolver(authority did.DID) did.ResolverFunc { + return func(_ context.Context, d did.DID) (did.Document, error) { + if err := did.ValidateMethod(d, Method); err != nil { + return did.Document{}, err + } + + doc := did.NewDocument(d) + vm := did.VerificationMethod{ + ID: doc.Fragment(authority.String()), + Controller: authority, + Type: attestation.Type, + Material: did.GenericMap{attestation.AuthorityProp: authority.String()}, + } + + if err := doc.VerificationMethods.Add(vm); err != nil { + return did.Document{}, err + } + + if err := doc.CapabilityDelegation.Add(vm); err != nil { + return did.Document{}, err + } + if err := doc.CapabilityInvocation.Add(vm); err != nil { + return did.Document{}, err + } + + return doc, nil + } +} diff --git a/attestation/go.mod b/attestation/go.mod new file mode 100644 index 00000000..97ea02dd --- /dev/null +++ b/attestation/go.mod @@ -0,0 +1,31 @@ +module github.com/fil-forge/forge/attestation + +go 1.27.0 + +require ( + github.com/alanshaw/dag-json-gen v0.0.9 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 + github.com/ipfs/go-cid v0.6.2 + github.com/multiformats/go-multihash v0.2.3 + github.com/multiformats/go-varint v0.1.0 + github.com/stretchr/testify v1.12.1 + github.com/whyrusleeping/cbor-gen v0.3.1 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da +) + +require ( + github.com/gobwas/glob v0.2.3 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multibase v0.3.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/sys v0.47.0 // indirect + lukechampine.com/blake3 v1.4.1 // indirect + pitr.ca/jsontokenizer v0.3.2 // indirect +) diff --git a/attestation/go.sum b/attestation/go.sum new file mode 100644 index 00000000..e1b3d62b --- /dev/null +++ b/attestation/go.sum @@ -0,0 +1,50 @@ +github.com/alanshaw/dag-json-gen v0.0.9 h1:q59Ra1mQB6HnFGI1ilFxsZvF1CqGN0XDlUWq16EJ6pw= +github.com/alanshaw/dag-json-gen v0.0.9/go.mod h1:v1YBZcS4B355MqxtyQr+fGNbEhm0CzHd+gOqOO/MZ+I= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +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-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +pitr.ca/jsontokenizer v0.3.2 h1:kzsKwfkWPV5XCmAF//hznh3L9IXbiL8aHbI0FswRkCs= +pitr.ca/jsontokenizer v0.3.2/go.mod h1:3DJdA2QNOU6cI0XkH6pRKZ4Oe8G5SDRUQ6PFAwaQ3YY= diff --git a/attestation/signer.go b/attestation/signer.go new file mode 100644 index 00000000..304947ed --- /dev/null +++ b/attestation/signer.go @@ -0,0 +1,69 @@ +package attestation + +import ( + "context" + "fmt" + + "github.com/fil-forge/forge/attestation/attest" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/fil-forge/ucantone/varsig" + "github.com/ipfs/go-cid" + mh "github.com/multiformats/go-multihash" +) + +func Attest(ctx context.Context, subject did.DID, authority ucan.Issuer) Issuer { + return Issuer{ + ctx: ctx, + did: subject, + authority: authority, + } +} + +type Issuer struct { + ctx context.Context + did did.DID + authority ucan.Issuer +} + +var _ ucan.Issuer = Issuer{} + +func (s Issuer) DID() did.DID { + return s.did +} + +func (s Issuer) String() string { + return fmt.Sprintf("%s (attested by %s)", s.did, s.authority.DID()) +} + +func (s Issuer) Sign(data []byte) []byte { + msgDigest, err := mh.Sum(data, mh.SHA2_256, -1) + if err != nil { + panic(fmt.Sprintf("failed to compute message digest: %v", err)) + } + + // The attestation is an assertion of fact: it is inherently idempotent + // (no nonce) and cannot expire (the attested delegation's own exp bounds + // the authorization). See the attested authority spec, signature + // invocation fields. + inv, err := attest.Proof.Invoke( + s.authority, + s.authority.DID(), + &attest.ProofArguments{Proof: cid.NewCidV1(cid.Raw, msgDigest)}, + invocation.WithNoNonce(), + invocation.WithNoExpiration(), + ) + if err != nil { + panic(fmt.Sprintf("failed to create invocation: %v", err)) + } + return inv.Bytes() +} + +func (s Issuer) SignatureAlgorithm() varsig.Algorithm { + return Algorithm{} +} + +func (s Issuer) Verifier() ucan.Verifier { + return AttestedVerifier(s.ctx, s.authority.DID(), s.authority.Verifier()) +} diff --git a/attestation/signer_test.go b/attestation/signer_test.go new file mode 100644 index 00000000..3c26b2ed --- /dev/null +++ b/attestation/signer_test.go @@ -0,0 +1,79 @@ +package attestation_test + +import ( + "bytes" + "testing" + + "github.com/ipfs/go-cid" + mh "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" + + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/did/key" + "github.com/fil-forge/ucantone/ucan/command" + "github.com/fil-forge/ucantone/ucan/delegation" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/fil-forge/ucantone/validator" + + "github.com/fil-forge/forge/attestation" + "github.com/fil-forge/forge/attestation/attest" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/ucantone/testutil" +) + +func TestSigner(t *testing.T) { + authority := testutil.RandomIssuer(t) + alice, err := did.Parse("did:mailto:example.com:alice") + require.NoError(t, err) + + issuer := attestation.Attest(t.Context(), alice, authority) + + del, err := delegation.Delegate( + issuer, + testutil.RandomDID(t), + issuer.DID(), + command.MustParse("/example/command"), + ) + require.NoError(t, err) + + t.Run("signs data correctly", func(t *testing.T) { + require.Equal(t, del.Signature().Header().SignatureAlgorithm(), attestation.Algorithm{}) + sigBytes := del.Signature().Bytes() + require.NotEmpty(t, sigBytes) + + inv, err := invocation.Decode(sigBytes) + require.NoError(t, err) + + require.Equal(t, authority.DID(), inv.Issuer()) + require.Equal(t, did.Undef, inv.Audience()) + require.Equal(t, authority.DID(), inv.Subject()) + require.Equal(t, attest.Proof.Command, inv.Command()) + + msgDigest, err := mh.Sum(del.SignedBytes(), mh.SHA2_256, -1) + require.NoError(t, err) + var proofArgs attest.ProofArguments + err = proofArgs.UnmarshalCBOR(bytes.NewReader(inv.ArgumentsBytes())) + require.NoError(t, err) + require.Equal(t, attest.ProofArguments{Proof: cid.NewCidV1(cid.Raw, msgDigest)}, proofArgs) + }) + + t.Run("delegation round-trips through CBOR and verifies", func(t *testing.T) { + encoded, err := delegation.Encode(del) + require.NoError(t, err) + + decoded, err := delegation.Decode(encoded) + require.NoError(t, err) + + resolver := did.ResolverMap{ + "key": key.Resolver, + "mailto": didmailto.NewResolver(authority.DID()), + } + factories := validator.DefaultFactories() + factories[attestation.Type] = attestation.NewVerifierFactory(resolver, factories) + err = validator.ValidateToken(t.Context(), decoded, + validator.WithDIDResolver(resolver), + validator.WithVerifierFactories(factories), + ) + require.NoError(t, err) + }) +} diff --git a/attestation/varsig.go b/attestation/varsig.go new file mode 100644 index 00000000..71630464 --- /dev/null +++ b/attestation/varsig.go @@ -0,0 +1,44 @@ +package attestation + +import ( + "fmt" + + "github.com/fil-forge/ucantone/varsig" + "github.com/fil-forge/ucantone/varsig/algorithm" + "github.com/multiformats/go-varint" +) + +func init() { + // Register spec-defined signature algorithms. + varsig.RegisterAlgorithmScheme(algorithm.AlgorithmDef{ + Code: VarsigCode, + Name: "Attested Authority", + Decoder: DecodeAlgoithm, + }) +} + +// VarsigCode is the Varsig signature algorithm code for attested signatures, under +// fil-one RFC 7. Note that the Varsig signature algorithm codes are *not* +// Multicodec codes! Officially, the Varsig code table makes no provision for +// extension, but we've selected a code in *Multicodec's* "private use" range, +// on the theory that it should be safe. +const VarsigCode uint64 = 0x300001 + +type Algorithm struct{} + +var algorithmInstance algorithm.Algorithm = Algorithm{} + +func (alg Algorithm) Encode() ([]byte, error) { + return varint.ToUvarint(VarsigCode), nil +} + +func DecodeAlgoithm(input []byte) (algorithm.Algorithm, int, error) { + code, n, err := varint.FromUvarint(input) + if err != nil { + return nil, 0, err + } + if code != VarsigCode { + return nil, n, fmt.Errorf("signature code is not attested-authority: 0x%02x, expected: 0x%02x", code, VarsigCode) + } + return algorithmInstance, n, nil +} diff --git a/attestation/verificationmethod.go b/attestation/verificationmethod.go new file mode 100644 index 00000000..0d55eec4 --- /dev/null +++ b/attestation/verificationmethod.go @@ -0,0 +1,83 @@ +package attestation + +import ( + "context" + "fmt" + "strings" + + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/validator" +) + +var ( + Type = "AuthorityAttestation" + AuthorityProp = "authority" +) + +// NewVerifierFactory returns a [validator.VerifierFactory] for +// AuthorityAttestation verification methods. Pass it to the validator via +// [validator.WithVerifierFactories]. The provided DID resolver and +// verifierFactories are used to derive verifiers for the authority's own +// verification methods. +func NewVerifierFactory(resolver did.Resolver, verifierFactories map[string]validator.VerifierFactory) validator.VerifierFactory { + return func(ctx context.Context, mat did.VerificationMaterial) (ucan.Verifier, error) { + authorityDidStr, ok := mat[AuthorityProp].(string) + if !ok { + return nil, fmt.Errorf("AuthorityAttestation verification method missing %s", AuthorityProp) + } + authorityDid, err := did.Parse(authorityDidStr) + if err != nil { + return nil, fmt.Errorf("failed to parse authority DID: %w", err) + } + doc, err := resolver.Resolve(ctx, authorityDid) + if err != nil { + return nil, fmt.Errorf("failed to resolve authority DID %s: %w", authorityDid, err) + } + + v, err := newMultiVerifier(ctx, verifierFactories, doc.CapabilityInvocation.All()) + if err != nil { + return nil, fmt.Errorf("failed to derive multi-verifier: %w", err) + } + return AttestedVerifier(ctx, authorityDid, v), nil + } +} + +// multiVerifier is a [ucan.Verifier] that verifies a signature if any of its +// component verifiers verify it. This is useful for cases where a token's +// issuer has multiple verification methods that could have been used to sign +// the token, and the verifier doesn't know which one was used. +type multiVerifier []ucan.Verifier + +func (m multiVerifier) Verify(data []byte, sig []byte) bool { + for _, v := range m { + if v.Verify(data, sig) { + return true + } + } + return false +} + +func (m multiVerifier) String() string { + var str strings.Builder + for _, v := range m { + str.WriteString(v.String()) + } + return fmt.Sprintf("multiVerifier{%d verifiers: %s}", len(m), str.String()) +} + +func newMultiVerifier(ctx context.Context, registry map[string]validator.VerifierFactory, vms []did.VerificationMethod) (ucan.Verifier, error) { + verifiers := make([]ucan.Verifier, 0, len(vms)) + for _, vm := range vms { + f, ok := registry[vm.Type] + if !ok { + return nil, fmt.Errorf("%w for VM type %q", validator.ErrNoVerifierFactory, vm.Type) + } + v, err := f(ctx, vm.Material) + if err != nil { + return nil, fmt.Errorf("deriving verifier for VM %s: %w", vm.ID, err) + } + verifiers = append(verifiers, v) + } + return multiVerifier(verifiers), nil +} diff --git a/attestation/verifier.go b/attestation/verifier.go new file mode 100644 index 00000000..b545a28e --- /dev/null +++ b/attestation/verifier.go @@ -0,0 +1,76 @@ +package attestation + +import ( + "bytes" + "context" + "fmt" + + "github.com/ipfs/go-cid" + mh "github.com/multiformats/go-multihash" + + "github.com/fil-forge/forge/attestation/attest" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/fil-forge/ucantone/ucan/token" +) + +// Verifier is a ucan.Verifier for a DID whose signing is attested by an +// authority verifier. +type Verifier struct { + ctx context.Context + authorityID did.DID + authorityVerifier ucan.Verifier +} + +var _ ucan.Verifier = Verifier{} + +func (v Verifier) String() string { + return fmt.Sprintf("Attested Verifier{authority=%s}", v.authorityID) +} + +func (v Verifier) Verify(msg []byte, sig []byte) bool { + inv, err := invocation.Decode(sig) + if err != nil { + return false + } + + var args attest.ProofArguments + err = args.UnmarshalCBOR(bytes.NewReader(inv.ArgumentsBytes())) + if err != nil { + return false + } + + msgDigest, err := mh.Sum(msg, mh.SHA2_256, -1) + if err != nil { + return false + } + + if args.Proof != cid.NewCidV1(cid.Raw, msgDigest) { + return false + } + + if inv.Subject() != v.authorityID { + return false + } + + // Verify the attestation invocation was actually signed by the authority, + // using the already-resolved authority verifier directly. Previously this + // re-validated via validator.ValidateInvocation with no options, which + // defaults to a did:key-only resolver and therefore fails to resolve a + // did:web authority — the cause of "signature mismatch" for did:web services + // (e.g. did:web:upload) while did:key authorities (and unit tests) passed. + if !token.VerifySignature(inv, v.authorityVerifier) { + return false + } + + return true +} + +func AttestedVerifier(ctx context.Context, authorityID did.DID, authority ucan.Verifier) ucan.Verifier { + return Verifier{ + ctx: ctx, + authorityID: authorityID, + authorityVerifier: authority, + } +} diff --git a/attestation/verifier_web_test.go b/attestation/verifier_web_test.go new file mode 100644 index 00000000..c0257068 --- /dev/null +++ b/attestation/verifier_web_test.go @@ -0,0 +1,88 @@ +package attestation_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/did/key" + "github.com/fil-forge/ucantone/multikey" + "github.com/fil-forge/ucantone/multikey/ed25519" + "github.com/fil-forge/ucantone/ucan/command" + "github.com/fil-forge/ucantone/ucan/delegation" + "github.com/fil-forge/ucantone/validator" + + "github.com/fil-forge/forge/attestation" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/ucantone/testutil" +) + +// TestSigner_WebAuthority exercises an attestation whose authority is a did:web +// service (e.g. did:web:upload), resolved via its DID document — the real +// service flow. The existing TestSigner uses a did:key authority, which the +// default key.Resolver handles, so it never caught that the attestation verifier +// re-resolved the authority with the did:key-only default resolver. With a +// did:web authority that path fails ("signature mismatch"); this test guards the +// fix that verifies with the already-resolved authority verifier. +func TestSigner_WebAuthority(t *testing.T) { + authority := multikey.NewIssuer(did.MustParse("did:web:example.com"), testutil.Must(ed25519.Generate())(t)) + doc := webDocument(t, authority) + + alice, err := did.Parse("did:mailto:example.com:alice") + require.NoError(t, err) + + issuer := attestation.Attest(t.Context(), alice, authority) + + del, err := delegation.Delegate( + issuer, + testutil.RandomDID(t), + issuer.DID(), + command.MustParse("/example/command"), + ) + require.NoError(t, err) + + encoded, err := delegation.Encode(del) + require.NoError(t, err) + decoded, err := delegation.Decode(encoded) + require.NoError(t, err) + + // Serve the authority's generated did:web document. + webResolver := did.ResolverFunc(func(_ context.Context, d did.DID) (did.Document, error) { + if d == authority.DID() { + return doc, nil + } + return did.Document{}, fmt.Errorf("unexpected did %s", d) + }) + resolver := did.ResolverMap{ + "key": key.Resolver, + "web": webResolver, + "mailto": didmailto.NewResolver(authority.DID()), + } + factories := validator.DefaultFactories() + factories[attestation.Type] = attestation.NewVerifierFactory(resolver, factories) + + err = validator.ValidateToken(t.Context(), decoded, + validator.WithDIDResolver(resolver), + validator.WithVerifierFactories(factories), + ) + require.NoError(t, err) +} + +// webDocument builds the DID document a did:web service would publish for +// itself: one verification method derived from the issuer's key, listed under +// authentication and assertionMethod. +func webDocument(t *testing.T, iss multikey.Issuer) did.Document { + t.Helper() + doc := did.NewDocument(iss.DID()) + verifier, ok := iss.Verifier().(multikey.Verifier) + require.True(t, ok, "issuer must have a multikey verifier") + vm := multikey.DeriveVerificationMethod(doc.Fragment("key-0"), verifier) + vm.Controller = doc.ID + require.NoError(t, doc.VerificationMethods.Add(vm)) + require.NoError(t, doc.Authentication.Add(vm)) + require.NoError(t, doc.AssertionMethod.Add(vm)) + return doc +} diff --git a/delegator/.dockerignore b/delegator/.dockerignore new file mode 100644 index 00000000..dbede3db --- /dev/null +++ b/delegator/.dockerignore @@ -0,0 +1,36 @@ +# Deployment +deploy/.env.production.local.tpl +deploy/.env.terraform.tpl +deploy/.env.terraform +deploy/Makefile +.terraform +deploy/.tfworkspace +deploy/app +deploy/shared + +# Git/CI +.github +.git +.gitignore + +# Documentation +*.md +LICENSE + +# Docker tooling +docker-compose*.yml + +# Environment files +.env* + +# IDE/Editor +.idea +.vscode +*.swp + +# Build artifacts +bin/ + +# Config (service uses env vars) +*.yaml +*.yml diff --git a/delegator/.gitignore b/delegator/.gitignore new file mode 100644 index 00000000..e894c1b8 --- /dev/null +++ b/delegator/.gitignore @@ -0,0 +1,36 @@ +# keys +*.pem +*.hex +# Binaries +delegator +*.exe +*.dll +*.so +*.dylib + +# Test binary +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Config files (except examples) +.delegator.yaml +!.delegator.example.yaml diff --git a/delegator/AGENTS.md b/delegator/AGENTS.md new file mode 100644 index 00000000..e2dc8ce6 --- /dev/null +++ b/delegator/AGENTS.md @@ -0,0 +1,189 @@ +# AGENTS.md — delegator + +Guidance for engineers and AI agents working in this repo. The code is ground +truth; if this file disagrees with the code, trust the code and update this file. + +## Purpose + +Go HTTP service that manages storage provider onboarding for the network: + +- Validates a registering provider's identity (DID allowlist, endpoint check, + UCAN proof validation) and records the registration in DynamoDB. +- Issues UCAN delegation chains that let registered storage nodes invoke + `/claim/cache` on the indexing service and `/space/egress/track` on the + egress tracking service. +- Handles on-chain contract approval of providers via the provider registry + smart contract (through `forgectl`). + +Module: `github.com/fil-forge/forge/delegator` (Go 1.27). + +## Build / Test / Run + +```bash +make build # go build -o bin/delegator ./main.go +make test # go test -v ./... +make run # build + run +./bin/delegator serve --host 0.0.0.0 --port 8080 +``` + +System tests live in `test/system_test.go` (~1000 lines): full server +lifecycle, all endpoints, end-to-end registration, mock storage node +interactions, concurrency, validation failures. + +```bash +go test -v ./test/... # system tests only +go test -v ./test/... -run TestSystemRegistrationFlow +``` + +## Layout + +``` +main.go # entry point +cmd/ # Cobra CLI: root ("registrar"), serve, store, gen + serve.go # start the HTTP server + store.go # store allow-did / disallow-did (allowlist management) + gen.go # generate a UCAN delegation +internal/ + config/config.go # Viper config structs (server, store, delegator, contract operator) + server/server.go # Echo server + route table + handlers/handlers.go # HTTP handlers (thin; delegate to registrar service) + services/registrar/ # Core logic: registration, proof validation, delegation minting, + delegator.go # contract approval + providers/providers.go # uber/fx dependency providers (signer, DIDs, proofs, contract operator) + store/ # DynamoDB persistence (Store interface, DynamoDB impl) +client/client.go # Go client library for external consumers +test/system_test.go # system test suite +deploy/ # Terraform (app/ + shared/) using storoku modules +``` + +## HTTP API + +Routes are wired in `internal/server/server.go`: + +| Method | Path | Purpose | +|--------|------|---------| +| GET | `/` | Connectivity check (plain "hello") | +| GET | `/healthcheck` | Health check | +| GET | `/.well-known/did.json` | Service DID document | +| PUT | `/registrar/register-node` | Register a storage provider | +| POST | `/registrar/request-approval` | On-chain contract approval of a provider | +| GET | `/registrar/request-proof` | DEPRECATED single-proof variant — remove when unused | +| GET | `/registrar/request-proofs` | Delegation proofs (indexer + egress tracker) | +| GET | `/registrar/is-registered` | Check registration status | + +`request-proofs` returns two gzip-encoded UCAN containers +(`container.RawGzip`): one for the indexing service, one for the egress +tracker (`egress_tracker` JSON field). + +## UCAN model (ucantone, not go-ucanto) + +This repo uses the `github.com/fil-forge/ucantone` UCAN implementation and +capability commands from `github.com/fil-forge/forge/protocol/commands/...`. +Capabilities are identified by *commands* with leading-slash paths +(e.g. `/blob/allocate`), not the older `domain/verb` capability strings. + +**Validated on registration** — the provider must delegate these commands to +the upload service (see `requiredProofs` in +`internal/services/registrar/delegator.go`): + +- `/blob/allocate`, `/blob/accept` (blob commands) +- `/blob/replica/allocate` (replica commands) +- `/pdp/info` (pdp commands) + +**Issued to registered nodes** — the delegator extends proofs it holds by one +hop, keeping the original subject so the chain resolves at invocation time: + +- `/claim/cache`: indexing service → delegator → storage node +- `/space/egress/track`: egress tracking service → delegator → storage node + +Each returned container carries both the root proof and the new hop. +Delegations are minted with `WithNoExpiration()`. + +## Registration validation (`Service.Register`) + +1. DID must be in the DynamoDB allowlist (`ErrDIDNotAllowed` otherwise). +2. DID must not already be registered. +3. The provider's public URL must serve its DID at the root endpoint — + accepted as either JSON `{"did": "..."}` or the storage node's plain-text + banner containing a `did:` line (see `assertEndpointServesDID`; a dedicated + DID endpoint on storage nodes is a known TODO). +4. The submitted proof container must delegate all `requiredProofs` commands + to the configured upload service DID. + +## Contract approval (`Service.RequestContractApproval`) + +Marked in code as a temporary solution pending a billing/operations design +decision. Flow: allowlist check → signature verification (provider signs its +own DID with its private key) → confirm registration with the registry smart +contract → check existing approval → submit approval transaction if needed. +The contract-level approval call is NOT idempotent; the pre-check exists to +avoid duplicate-approval failures. On-chain access goes through +`forgectl/pkg/services/{chain,inspector,operator}` wrapped by +`SmartContractOperator` in `internal/providers/providers.go`. + +## Storage + +DynamoDB via `internal/store/dynamo.go`, two tables: + +- allowlist table (`store.allowlist_table_name`) +- provider info table (`store.providerinfo_table_name`) + +The store creates missing tables on initialization — convenient for local +DynamoDB, relevant to IAM permissions in real deployments. Local dev: + +```bash +docker run -p 8000:8000 amazon/dynamodb-local -jar DynamoDBLocal.jar -sharedDb +export REGISTRAR_STORE_ENDPOINT=localhost:8000 +``` + +## Configuration + +Precedence: flags > env vars > config file (`.delegator.yaml`). Env vars use +the `REGISTRAR_` prefix (set in `cmd/root.go`), e.g. `REGISTRAR_SERVER_PORT`. +Key sections (see `internal/config/config.go`): + +- `server`: host, port +- `store`: region, table names, provider weight, optional endpoint +- `delegator`: signing key (inline or file), DID, indexing/egress-tracking + service DIDs and proofs (inline or file), upload service DID +- `contract`: chain RPC endpoint, payments/service/registry contract + addresses, transactor (chain ID, key or keystore) + +The service identity is an Ed25519 key; the indexing and egress-tracking +proofs are delegations *to* this delegator that it re-delegates to nodes. + +## Key dependencies + +- `github.com/fil-forge/ucantone` — UCAN (delegations, containers, DIDs) +- `github.com/fil-forge/forge/protocol` — capability command definitions + (ucantone/protocol replaced the older go-ucanto/go-libstoracha stack — + older docs referencing those are stale) +- `github.com/fil-forge/forgectl` — smart contract operations +- `github.com/ethereum/go-ethereum` — addresses, contract bindings, signatures +- `github.com/labstack/echo/v4` — HTTP server +- `go.uber.org/fx` — dependency injection (all wiring in `internal/providers`) +- `github.com/spf13/cobra` + `viper` — CLI and config +- `github.com/aws/aws-sdk-go-v2` — DynamoDB + +## Deployment + +Terraform in `deploy/app` and `deploy/shared`, built on the storoku modules +(`github.com/storacha/storoku//{app,shared}`). `deploy/app/external.tf` grants +the ECS task DynamoDB access. A `Dockerfile` builds the service image. + +## Gotchas / blast radius + +- **Delegation format changes affect all downstream consumers** — storage + nodes (piri), the indexing service, and the egress tracker all consume the + proofs minted here. Changing container encoding, chain shape, or commands + can break nodes in the field. +- **Proof validation changes can lock out providers** — tightening + `requiredProofs` or validation logic affects storage node onboarding and + re-registration. +- The legacy `GET /registrar/request-proof` endpoint is deprecated but still + routed; do not remove it without confirming no node still calls it. +- Endpoint DID verification parses the storage node's plain-text banner as a + fallback; changes to the node's root-endpoint output format can break + registration. +- Naming quirk: the Cobra root command and env prefix are `registrar`/ + `REGISTRAR_`, while the binary/module is `delegator`. diff --git a/delegator/CLAUDE.md b/delegator/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/delegator/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/delegator/Makefile b/delegator/Makefile new file mode 100644 index 00000000..158dabb2 --- /dev/null +++ b/delegator/Makefile @@ -0,0 +1,43 @@ +.PHONY: build test clean run help + +# Default target - just build +build: + go build -o bin/delegator ./main.go + +test: + go test -v ./... + +clean: + rm -rf bin/ + go clean + +run: build + ./bin/delegator + +help: + @echo "Available targets:" + @echo " make - Build the application (default)" + @echo " make build - Build the application" + @echo " make test - Run all tests" + @echo " make clean - Clean build artifacts" + @echo " make run - Build and run the application" + @echo " make help - Show this help message" +# --- monorepo verb contract (see the root Makefile): vet, image, image-dev --- +# Resolve the docker binary to an absolute path: directories named 'docker' +# on PATH (including this repo's own docker/ when '.' is in PATH) break make's +# direct exec of a bare 'docker'. +DOCKER := $(shell command -v docker) + +# Container image via the monorepo-shared docker/Dockerfile (context = the repo root). +IMAGE_BUILD_ARGS := --build-arg SERVICE=delegator --build-arg MAIN_PKG=. --build-arg BIN=registrar + +.PHONY: vet image image-dev +vet: + go vet ./... + +image: + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/delegator:local .. + +image-dev: + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/delegator:local-dev .. + diff --git a/delegator/README.md b/delegator/README.md new file mode 100644 index 00000000..502d8a39 --- /dev/null +++ b/delegator/README.md @@ -0,0 +1,255 @@ +# Delegator HTTP Server + +A delegation service that manages storage provider registration and proof generation for the Storacha network. The service validates provider identities, manages their registration status, and issues delegations for interaction with indexing services. + +## Architecture + +Built with: +- **Cobra** - CLI framework for command management +- **Viper** - Configuration management (files, env vars, flags) +- **Uber.fx** - Dependency injection framework +- **Echo** - High-performance HTTP server +- **DynamoDB** - Persistent storage for provider registry + +## Quick Start + +```bash +# Build the server +go build -o delegator + +# Run the server +./delegator serve + +# Or with custom host/port +./delegator serve --host 127.0.0.1 --port 9090 +``` + +## API Endpoints + +### Health & Status + +#### `GET /` +Simple root endpoint for basic connectivity check. + +**Response:** +- `200 OK` - Returns plain text "hello" + +#### `GET /healthcheck` +Health check endpoint for monitoring service availability. + +**Response:** +- `200 OK` - Service is healthy +```json +{ + "status": "healthy" +} +``` + +### Provider Registration + +#### `PUT /registrar/register-node` +Register a storage provider node with the delegator service. Validates the provider's identity, proof, and endpoint availability. + +**Request Body:** +```json +{ + "did": "did:key:z6MksvRCPWoXvMj8sUzuHiQ4pFkSawkKRz2eh1TALNEG6s3e", + "owner_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", + "proof_set_id": 1, + "operator_email": "operator@example.com", + "public_url": "https://storage.example.com", + "proof": "" +} +``` + +**Validation:** +- DID must be in the allow list +- DID must not already be registered +- Public URL must serve the provider's DID at root endpoint +- Proof must be valid delegation from provider to upload service with required capabilities: + - `blob/accept` + - `blob/allocate` + - `blob/replica/allocate` + - `pdp/info` + +**Response:** +- `201 Created` - Successfully registered +- `400 Bad Request` - Invalid request data or unauthorized DID +- `500 Internal Server Error` - Server-side processing error + +#### `GET /registrar/is-registered` +Check if a provider DID is registered with the service. + +**Request Body:** +```json +{ + "did": "did:key:z6MksvRCPWoXvMj8sUzuHiQ4pFkSawkKRz2eh1TALNEG6s3e" +} +``` + +**Response:** +- `200 OK` - Provider is registered +- `404 Not Found` - Provider is not registered +- `400 Bad Request` - Invalid DID format + +#### `GET /registrar/request-proof` +Request a delegation proof for a registered provider to interact with the indexing service. + +**Request Body:** +```json +{ + "did": "did:key:z6MksvRCPWoXvMj8sUzuHiQ4pFkSawkKRz2eh1TALNEG6s3e" +} +``` + +**Response:** +- `200 OK` - Returns delegation proof +```json +{ + "proof": "" +} +``` +- `400 Bad Request` - Invalid DID or provider not registered/authorized + +The returned proof grants the provider the `claim/cache` capability for the indexing service. + +## Client Library + +A Go client library is provided in the `client/` package for interacting with the delegator service: + +```go +import "github.com/fil-forge/forge/internal/client/delegator" + +// Create client +c, err := client.New("http://localhost:8080") + +// Register a provider +err = c.Register(ctx, &client.RegisterRequest{ + DID: "did:key:...", + OwnerAddress: "0x...", + ProofSetID: 1, + OperatorEmail: "operator@example.com", + PublicURL: "https://storage.example.com", + Proof: proofString, +}) + +// Check registration status +registered, err := c.IsRegistered(ctx, &client.IsRegisteredRequest{ + DID: "did:key:...", +}) + +// Request proof +resp, err := c.RequestProof(ctx, "did:key:...") +``` + +## Testing + +### Run All Tests +```bash +# Run all tests including system tests +go test ./... + +# Run with verbose output +go test -v ./... + +# Run only system tests +go test -v ./test/... +``` + +### System Tests +The comprehensive system test suite in `test/system_test.go` covers: +- Full server lifecycle testing +- All API endpoints with success and failure scenarios +- End-to-end registration workflow +- Mock storage node interactions +- Concurrent request handling +- Input validation and error cases + +### Test Individual Components +```bash +# Test specific functionality +go test -v ./test/... -run TestSystemRegistrationFlow +go test -v ./test/... -run TestSystemRequestProof +``` + +### Test Coverage +```bash +# Generate coverage report +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out +``` + +## Configuration + +The server can be configured through multiple sources (in order of precedence): +1. Command line flags +2. Environment variables +3. Configuration file (`.delegator.yaml`) + +### Configuration File Example +```yaml +server: + host: 0.0.0.0 + port: 8080 + +store: + region: us-east-1 + allowlist_table_name: delegator-allowlist + providerinfo_table_name: delegator-providers + providerweight: 100 + endpoint: "" # Optional: for local DynamoDB + +delegator: + key_file: /path/to/private/key.pem + indexing_service_web_did: did:web:indexing.example.com + indexing_service_proof: "" + upload_service_did: did:key:uploadservice... +``` + +### Environment Variables +All configuration values can be set via environment variables with the prefix `REGISTRAR_`: +```bash +export REGISTRAR_SERVER_PORT=9090 +export REGISTRAR_STORE_REGION=us-west-2 +``` + +## Development + +### Prerequisites +- Go 1.27 or later +- Access to DynamoDB (AWS or local) +- Ed25519 private key for delegator identity + +### Local Development with Docker +```bash +# Run local DynamoDB +docker run -p 8000:8000 amazon/dynamodb-local -jar DynamoDBLocal.jar -sharedDb + +# Configure for local development +export REGISTRAR_STORE_ENDPOINT=localhost:8000 +``` + +## Container images + +A push to `main` publishes to GHCR from the `Container` workflow, as +`ghcr.io/fil-forge/delegator:main` plus a `sha-` tag, covering +`linux/amd64` and `linux/arm64`. Release tags publish the same image under their +semver version. + +## Deploying to dev + +The `main` run also asks [infra-central][] to deploy what it just published. It +dispatches a `bump-deployed-image` event carrying the manifest digest, and +infra-central's [Bump deployed image][receiver] workflow opens a pull request +pinning that digest in `terraform/envs/dev/apps/terraform.tfvars`, with +auto-merge enabled. infra-central's [Check and deploy][deploy] workflow runs +`tofu apply` on `dev/apps` on every push to its `main`, so merging that pull +request is what deploys. + +The dispatch runs as the `fil-forge-bot` GitHub App and needs the +`FORGE_BOT_APP_ID` variable and the `FORGE_BOT_PRIVATE_KEY` secret. Prod pins +are promoted by hand. + +[infra-central]: https://github.com/fil-forge/infra-central +[receiver]: https://github.com/fil-forge/infra-central/blob/main/.github/workflows/bump-deployed-image.yml +[deploy]: https://github.com/fil-forge/infra-central/blob/main/.github/workflows/check-and-deploy.yml diff --git a/delegator/cmd/gen.go b/delegator/cmd/gen.go new file mode 100644 index 00000000..50eb2da6 --- /dev/null +++ b/delegator/cmd/gen.go @@ -0,0 +1,124 @@ +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/claim" + "github.com/fil-forge/forge/protocol/commands/space/egress" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/multikey" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/delegation" + "github.com/spf13/cobra" +) + +var GenCmd = &cobra.Command{ + Use: "gen", + Aliases: []string{"g"}, + Short: "Generate a UCAN delegation", + SilenceUsage: true, + Args: cobra.NoArgs, + RunE: mkDelegation, +} + +var ( + // Gen command flags + issuerPrivateKeyFile string + issuerDidWebKey string + audienceDidKey string + command string + expiration int64 +) + +func init() { + GenCmd.Flags().StringVarP(&issuerPrivateKeyFile, "issuer-private-key-file", "f", "", "Path to PEM encoded Ed25519 private key of delegation issuer") + cobra.CheckErr(GenCmd.MarkFlagRequired("issuer-private-key-file")) + + GenCmd.Flags().StringVarP(&issuerDidWebKey, "issuer-did-web", "i", "", "Optional did:web: of issuer, when provided wraps did:key: of delegation issuer") + + GenCmd.Flags().StringVarP(&audienceDidKey, "audience-did-key", "a", "", "did:key of delegation audience") + cobra.CheckErr(GenCmd.MarkFlagRequired("audience-did-key")) + + GenCmd.Flags().StringVarP(&command, "command", "c", "", "command issuer will authorize to audience") + cobra.CheckErr(GenCmd.MarkFlagRequired("command")) + + GenCmd.Flags().Int64VarP(&expiration, "expiration", "e", 0, "expiration time in UTC seconds since Unix epoch") +} + +func mkDelegation(cmd *cobra.Command, _ []string) error { + signer, err := parseIssuerKey(issuerPrivateKeyFile) + if err != nil { + return fmt.Errorf("parsing issuer private key from file %s: %w", issuerPrivateKeyFile, err) + } + + issuer := multikey.KeyIssuer(signer) + if issuerDidWebKey != "" { + issuerDidWeb, err := did.Parse(issuerDidWebKey) + if err != nil { + return fmt.Errorf("parsing issuer did web key (%s): %w", issuerDidWebKey, err) + } + if issuerDidWeb.Method() != "web" { + return fmt.Errorf("issuer did:web: must start with 'did:web:' prefix") + } + issuer = multikey.NewIssuer(issuerDidWeb, signer) + } + + audience, err := did.Parse(audienceDidKey) + if err != nil { + return fmt.Errorf("parsing audience did key: %w", err) + } + + var opts []delegation.Option + if expiration > 0 { + if time.Now().Unix() > expiration { + return fmt.Errorf("provided expiration time %d is in the past", expiration) + } + opts = append(opts, delegation.WithExpiration(ucan.UnixTimestamp(expiration))) + } else { + opts = append(opts, delegation.WithNoExpiration()) + } + + // Subject must be the issuer's own DID — the issuer is delegating + // authority over its own resources to the audience. Using `audience` + // here produces a delegation whose subject is the delegator (not the + // indexer/etracker), which fails downstream chain validation with + // "delegation subject is X not Y" when piri later uses this as a + // proof for invoking against the indexing/egress-tracker service. + subject := issuer.DID() + + var d ucan.Delegation + if command == claim.Cache.Command.String() { + d, err = claim.Cache.Delegate(issuer, audience, subject, opts...) + if err != nil { + return fmt.Errorf("creating delegation: %w", err) + } + } else if command == egress.Track.Command.String() { + d, err = egress.Track.Delegate(issuer, audience, subject, opts...) + if err != nil { + return fmt.Errorf("creating delegation: %w", err) + } + } else { + return fmt.Errorf("unknown command: %s", command) + } + + out, err := delegation.Encode(d) + if err != nil { + return fmt.Errorf("formatting delegation: %w", err) + } + fmt.Println(string(out)) + return nil + +} + +// parseIssuerKey attempts to read and parse the private key from the +// provided path. +func parseIssuerKey(path string) (multikey.Signer, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading file: %w", err) + } + return identity.DecodeSignerFromPEM(data) +} diff --git a/delegator/cmd/root.go b/delegator/cmd/root.go new file mode 100644 index 00000000..acac4765 --- /dev/null +++ b/delegator/cmd/root.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile string + +var RootCmd = &cobra.Command{ + Use: "registrar", + Short: "Registrar HTTP server", + Long: `A simple HTTP server with health check, register, and request-proof endpoints.`, +} + +func Execute() error { + return RootCmd.Execute() +} + +func init() { + cobra.OnInitialize(initConfig) + + RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.delegator.yaml)") + RootCmd.AddCommand(ServeCmd) + RootCmd.AddCommand(StoreCmd) + RootCmd.AddCommand(GenCmd) +} + +func initConfig() { + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else { + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + viper.AddConfigPath(home) + viper.AddConfigPath(".") + viper.SetConfigType("yaml") + viper.SetConfigName(".delegator") + } + + // Enable automatic environment variable binding + viper.SetEnvPrefix("REGISTRAR") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() + + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/delegator/cmd/serve.go b/delegator/cmd/serve.go new file mode 100644 index 00000000..6f26f991 --- /dev/null +++ b/delegator/cmd/serve.go @@ -0,0 +1,130 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.uber.org/fx" + + "github.com/fil-forge/forge/delegator/internal/config" + "github.com/fil-forge/forge/delegator/internal/handlers" + "github.com/fil-forge/forge/delegator/internal/providers" + "github.com/fil-forge/forge/delegator/internal/server" + "github.com/fil-forge/forge/delegator/internal/services/registrar" + "github.com/fil-forge/forge/delegator/internal/store" +) + +var ServeCmd = &cobra.Command{ + Use: "serve", + Short: "Start the HTTP server", + Long: `Start the registrar HTTP server with configured endpoints.`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := config.NewConfig() + if err != nil { + return err + } + app := fx.New( + // Configuration + config.SupplyConfigs(cfg), + fx.Provide( + // Providers for complex types + providers.ProvideSigner, + providers.ProvideIndexingServiceWebDID, + providers.ProvideIndexingServiceProof, + providers.ProvideEgressTrackingServiceDID, + providers.ProvideEgressTrackingServiceProof, + providers.ProvideUploadServiceDID, + providers.ProvideContractOperator, + + // Store + fx.Annotate( + store.NewDynamoDBStore, + fx.As(new(store.Store)), + ), + + // Service + registrar.New, + + // Handlers and Server + handlers.NewHandlers, + server.NewServer, + ), + fx.Invoke(server.Start), + ) + + app.Run() + return nil + }, +} + +const ( + FilecoinCalibrationNetworkChainID = 314159 + FilecoinCalibrationNetworkChainEndpoint = "https://api.calibration.node.glif.io/rpc/v1" +) + +func init() { + // Server flags + ServeCmd.Flags().String("host", "0.0.0.0", "Server host") + ServeCmd.Flags().Int("port", 8080, "Server port") + + // Store flags + ServeCmd.Flags().String("store-region", "", "AWS region for DynamoDB") + ServeCmd.Flags().String("store-allowlist-table", "", "DynamoDB table name for allowlist") + ServeCmd.Flags().String("store-providerinfo-table", "", "DynamoDB table name for provider info") + ServeCmd.Flags().Uint("store-provider-weight", 1, "Default weight for registered providers") + ServeCmd.Flags().String("store-endpoint", "", "DynamoDB endpoint (for local testing)") + + // Service flags + ServeCmd.Flags().String("delegator-key", "", "Multibase-encoded delegator private key") + ServeCmd.Flags().String("delegator-key-file", "", "Path to delegator private key file") + ServeCmd.Flags().String("delegator-did", "", "DID web of the delegator") + + ServeCmd.Flags().String("delegator-indexing-service-did", "", "DID of the indexing service") + ServeCmd.Flags().String("delegator-indexing-service-proof", "", "Base64-encoded proof from indexing service") + ServeCmd.Flags().String("delegator-indexing-service-proof-file", "", "Path to file containing proof from indexing service") + ServeCmd.Flags().String("delegator-egress-tracking-service-did", "", "DID of the egress tracking service") + ServeCmd.Flags().String("delegator-egress-tracking-service-proof", "", "Base64-encoded proof from egress tracking service") + ServeCmd.Flags().String("delegator-egress-tracking-service-proof-file", "", "Path to file containing proof from egress tracking service") + ServeCmd.Flags().String("delegator-upload-service-did", "", "DID of the upload service") + + // Contract operator flags + ServeCmd.Flags().String("contract-chain-client-endpoint", FilecoinCalibrationNetworkChainEndpoint, "Blockchain client RPC endpoint URL") + ServeCmd.Flags().String("contract-payments-contract-address", "", "Ethereum address of the payments contract") + ServeCmd.Flags().String("contract-service-contract-address", "", "Ethereum address of the service contract") + ServeCmd.Flags().String("contract-registry-contract-address", "", "Ethereum address of the registry contract") + ServeCmd.Flags().Int64("contract-transactor-chain-id", FilecoinCalibrationNetworkChainID, "Chain ID for blockchain transactions") + ServeCmd.Flags().String("contract-transactor-key", "", "Ethereum private key for transaction signing") + ServeCmd.Flags().String("contract-transactor-keystore-path", "", "Path to Ethereum keystore file for transaction signing") + ServeCmd.Flags().String("contract-transactor-keystore-password", "", "Password for the Ethereum keystore file") + + // Bind flags to viper + cobra.CheckErr(viper.BindPFlag("server.host", ServeCmd.Flags().Lookup("host"))) + cobra.CheckErr(viper.BindPFlag("server.port", ServeCmd.Flags().Lookup("port"))) + + cobra.CheckErr(viper.BindPFlag("store.region", ServeCmd.Flags().Lookup("store-region"))) + cobra.CheckErr(viper.BindPFlag("store.allowlist_table_name", ServeCmd.Flags().Lookup("store-allowlist-table"))) + // bind flag to storoku-style environment variable + cobra.CheckErr(viper.BindEnv("store.allowlist_table_name", "STORAGE_PROVIDER_ALLOW_LIST_TABLE_ID")) + cobra.CheckErr(viper.BindPFlag("store.providerinfo_table_name", ServeCmd.Flags().Lookup("store-providerinfo-table"))) + cobra.CheckErr(viper.BindPFlag("store.providerweight", ServeCmd.Flags().Lookup("store-provider-weight"))) + cobra.CheckErr(viper.BindPFlag("store.endpoint", ServeCmd.Flags().Lookup("store-endpoint"))) + + cobra.CheckErr(viper.BindPFlag("delegator.key", ServeCmd.Flags().Lookup("delegator-key"))) + cobra.CheckErr(viper.BindPFlag("delegator.key_file", ServeCmd.Flags().Lookup("delegator-key-file"))) + cobra.CheckErr(viper.BindPFlag("delegator.did", ServeCmd.Flags().Lookup("delegator-did"))) + cobra.CheckErr(viper.BindPFlag("delegator.indexing_service_web_did", ServeCmd.Flags().Lookup("delegator-indexing-service-did"))) + cobra.CheckErr(viper.BindPFlag("delegator.indexing_service_proof", ServeCmd.Flags().Lookup("delegator-indexing-service-proof"))) + cobra.CheckErr(viper.BindPFlag("delegator.indexing_service_proof_file", ServeCmd.Flags().Lookup("delegator-indexing-service-proof-file"))) + cobra.CheckErr(viper.BindPFlag("delegator.egress_tracking_service_did", ServeCmd.Flags().Lookup("delegator-egress-tracking-service-did"))) + cobra.CheckErr(viper.BindPFlag("delegator.egress_tracking_service_proof", ServeCmd.Flags().Lookup("delegator-egress-tracking-service-proof"))) + cobra.CheckErr(viper.BindPFlag("delegator.egress_tracking_service_proof_file", ServeCmd.Flags().Lookup("delegator-egress-tracking-service-proof-file"))) + cobra.CheckErr(viper.BindPFlag("delegator.upload_service_did", ServeCmd.Flags().Lookup("delegator-upload-service-did"))) + + cobra.CheckErr(viper.BindPFlag("contract.chain_client_endpoint", ServeCmd.Flags().Lookup("contract-chain-client-endpoint"))) + cobra.CheckErr(viper.BindPFlag("contract.payments_contract_address", ServeCmd.Flags().Lookup("contract-payments-contract-address"))) + cobra.CheckErr(viper.BindPFlag("contract.service_contract_address", ServeCmd.Flags().Lookup("contract-service-contract-address"))) + cobra.CheckErr(viper.BindPFlag("contract.registry_contract_address", ServeCmd.Flags().Lookup("contract-registry-contract-address"))) + cobra.CheckErr(viper.BindPFlag("contract.transactor.chain_id", ServeCmd.Flags().Lookup("contract-transactor-chain-id"))) + cobra.CheckErr(viper.BindPFlag("contract.transactor.key", ServeCmd.Flags().Lookup("contract-transactor-key"))) + cobra.CheckErr(viper.BindPFlag("contract.transactor.keystore_path", ServeCmd.Flags().Lookup("contract-transactor-keystore-path"))) + cobra.CheckErr(viper.BindPFlag("contract.transactor.keystore_password", ServeCmd.Flags().Lookup("contract-transactor-keystore-password"))) +} diff --git a/delegator/cmd/store.go b/delegator/cmd/store.go new file mode 100644 index 00000000..c6c5ad39 --- /dev/null +++ b/delegator/cmd/store.go @@ -0,0 +1,137 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/fil-forge/ucantone/did" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/fil-forge/forge/delegator/internal/config" + "github.com/fil-forge/forge/delegator/internal/store" +) + +var StoreCmd = &cobra.Command{ + Use: "store", + Short: "Manage store operations", + Long: `Commands for managing the DynamoDB store, including allowlist management.`, +} + +var allowDIDCmd = &cobra.Command{ + Use: "allow-did [did]", + Short: "Add a DID to the allowlist", + Long: `Add a DID to the allowlist. If the DID is already in the list, the command returns success (idempotent).`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + didStr := args[0] + + // Validate DID + parsedDID, err := did.Parse(didStr) + if err != nil { + return fmt.Errorf("invalid DID format: %w", err) + } + + // Initialize store + cfg := &config.Config{} + if err := viper.Unmarshal(cfg); err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + + db, err := store.NewDynamoDBStore(cfg.Store) + if err != nil { + return fmt.Errorf("failed to initialize store: %w", err) + } + + ctx := context.Background() + + // Check if DID is already allowed + allowed, err := db.IsAllowedDID(ctx, parsedDID) + if err != nil { + return fmt.Errorf("failed to check DID status: %w", err) + } + + if allowed { + fmt.Printf("DID %s is already allowed\n", didStr) + return nil + } + + // Add DID to allowlist + err = db.AddAllowedDID(ctx, parsedDID) + if err != nil { + return fmt.Errorf("failed to add DID to allowlist: %w", err) + } + + fmt.Printf("Successfully added DID %s to allowlist\n", didStr) + return nil + }, +} + +var disallowDIDCmd = &cobra.Command{ + Use: "disallow-did [did]", + Short: "Remove a DID from the allowlist", + Long: `Remove a DID from the allowlist. If the DID is not in the list, the command returns success (idempotent).`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + didStr := args[0] + + // Validate DID + parsedDID, err := did.Parse(didStr) + if err != nil { + return fmt.Errorf("invalid DID format: %w", err) + } + + // Initialize store + cfg := &config.Config{} + if err := viper.Unmarshal(cfg); err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + + db, err := store.NewDynamoDBStore(cfg.Store) + if err != nil { + return fmt.Errorf("failed to initialize store: %w", err) + } + + ctx := context.Background() + + // Check if DID is allowed + allowed, err := db.IsAllowedDID(ctx, parsedDID) + if err != nil { + return fmt.Errorf("failed to check DID status: %w", err) + } + + if !allowed { + fmt.Printf("DID %s is not in the allowlist\n", didStr) + return nil + } + + // Remove DID from allowlist + err = db.RemoveAllowedDID(ctx, parsedDID) + if err != nil { + return fmt.Errorf("failed to remove DID from allowlist: %w", err) + } + + fmt.Printf("Successfully removed DID %s from allowlist\n", didStr) + return nil + }, +} + +func init() { + // Add store flags (similar to serve command) + StoreCmd.PersistentFlags().String("store-region", "", "AWS region for DynamoDB") + StoreCmd.PersistentFlags().String("store-allowlist-table", "", "DynamoDB table name for allowlist") + StoreCmd.PersistentFlags().String("store-providerinfo-table", "", "DynamoDB table name for provider info") + StoreCmd.PersistentFlags().Uint("store-provider-weight", 1, "Default weight for registered providers") + StoreCmd.PersistentFlags().String("store-endpoint", "", "DynamoDB endpoint (for local testing)") + + // Bind flags to viper + cobra.CheckErr(viper.BindPFlag("store.region", StoreCmd.PersistentFlags().Lookup("store-region"))) + cobra.CheckErr(viper.BindPFlag("store.allowlist_table_name", StoreCmd.PersistentFlags().Lookup("store-allowlist-table"))) + cobra.CheckErr(viper.BindPFlag("store.providerinfo_table_name", StoreCmd.PersistentFlags().Lookup("store-providerinfo-table"))) + cobra.CheckErr(viper.BindPFlag("store.providerweight", StoreCmd.PersistentFlags().Lookup("store-provider-weight"))) + cobra.CheckErr(viper.BindPFlag("store.endpoint", StoreCmd.PersistentFlags().Lookup("store-endpoint"))) + + // Add subcommands + StoreCmd.AddCommand(allowDIDCmd) + StoreCmd.AddCommand(disallowDIDCmd) +} diff --git a/delegator/go.mod b/delegator/go.mod new file mode 100644 index 00000000..47b516e0 --- /dev/null +++ b/delegator/go.mod @@ -0,0 +1,113 @@ +module github.com/fil-forge/forge/delegator + +go 1.27.0 + +require ( + github.com/aws/aws-sdk-go-v2 v1.45.1 + github.com/aws/aws-sdk-go-v2/config v1.33.2 + github.com/aws/aws-sdk-go-v2/credentials v1.20.2 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.66.0 + github.com/ethereum/go-ethereum v1.17.5 + github.com/fil-forge/forge/internal v0.0.0 + github.com/fil-forge/forge/protocol v0.0.0 + github.com/fil-forge/forgectl v0.0.0-20260507183024-793714651bc2 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 + github.com/ipfs/go-log/v2 v2.9.2 + github.com/labstack/echo/v4 v4.13.4 + github.com/spf13/cobra v1.10.2 + github.com/spf13/viper v1.21.0 + go.uber.org/fx v1.24.0 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/alanshaw/dag-json-gen v0.0.9 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.36.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.41.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.48.0 // indirect + github.com/aws/smithy-go v1.28.1 // indirect + github.com/bits-and-blooms/bitset v1.22.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.8 // indirect + github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 // indirect + github.com/filecoin-project/go-data-segment v0.0.1 // indirect + github.com/fjl/jsonw v0.1.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/ipfs/go-cid v0.6.2 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multibase v0.3.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/prometheus/client_golang v1.23.0 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/supranational/blst v0.3.16 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.uber.org/dig v1.19.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + lukechampine.com/blake3 v1.4.1 // indirect + pitr.ca/jsontokenizer v0.3.2 // indirect +) + +replace github.com/fil-forge/forge/protocol => ../protocol + +replace github.com/fil-forge/forge/internal => ../internal diff --git a/delegator/go.sum b/delegator/go.sum new file mode 100644 index 00000000..b6ad0180 --- /dev/null +++ b/delegator/go.sum @@ -0,0 +1,367 @@ +github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= +github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/RaduBerinde/axisds v0.1.0 h1:YItk/RmU5nvlsv/awo2Fjx97Mfpt4JfgtEVAGPrLdz8= +github.com/RaduBerinde/axisds v0.1.0/go.mod h1:UHGJonU9z4YYGKJxSaC6/TNcLOBptpmM5m2Cksbnw0Y= +github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54 h1:bsU8Tzxr/PNz75ayvCnxKZWEYdLMPDkUgticP4a4Bvk= +github.com/RaduBerinde/btreemap v0.0.0-20250419174037-3d62b7205d54/go.mod h1:0tr7FllbE9gJkHq7CVeeDDFAFKQVy5RnCSSNBOvdqbc= +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/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/alanshaw/dag-json-gen v0.0.9 h1:q59Ra1mQB6HnFGI1ilFxsZvF1CqGN0XDlUWq16EJ6pw= +github.com/alanshaw/dag-json-gen v0.0.9/go.mod h1:v1YBZcS4B355MqxtyQr+fGNbEhm0CzHd+gOqOO/MZ+I= +github.com/aws/aws-sdk-go-v2 v1.45.1 h1:iIoG3NaLhV6UZpPXyPXlDj2I9oS8tV/nMcMnITCC6Ks= +github.com/aws/aws-sdk-go-v2 v1.45.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU= +github.com/aws/aws-sdk-go-v2/config v1.33.2 h1:Pj4+nF2kc4Z+1BJysVPnX9d5dMN7IYFXR4UJaWK2IpA= +github.com/aws/aws-sdk-go-v2/config v1.33.2/go.mod h1:Igw+HTwbR2tsTU/ydifAS9EHAFJ2s/FCgkwQWFnAdE4= +github.com/aws/aws-sdk-go-v2/credentials v1.20.2 h1:VQjZODPNfdikCX2ZZrltw4zNLkcwjyUFDUl2vT9yTwg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.2/go.mod h1:OmeHCn28vZylsBvalLDf7t8fuJ2rHYQprJs+7WuxniI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 h1:YIEBqcqRnpi4Pfv0YHImtgi6czGCwKHANC7SwmUAVD0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1/go.mod h1:imEf0oufgAo8KAkCHhrOdqGEC0YWx1PPBQH82shSxGw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 h1:pc138gM1CW+XPc60rEwUlwwuwWFQK16CI1T7v1F9Oec= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1/go.mod h1:1+koxpPIbfBdfzP6vojm5/zTpTQ/micYwlxIiNB3TxI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 h1:K0JsbZQj+1h208Ro1zHeA4l7bMp0NvRffHQ91q8Ol1s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1/go.mod h1:W3/vL6EtCIatICGy9ab29QhMuae+cOKPWcMxv02CO+Q= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 h1:yhw5KD1phVyP9vijxOUzDfEtJx+bt+L63k+VfuiYFAA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1/go.mod h1:ZW2e0d7DYlRxlS9hEiMXE47gTdX5KRN4byUiNbUpG+Q= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.66.0 h1:Iz90Uc1I8Yx/EVZMVEdB3SuLRkOJYY1YUUU0hTV7AFo= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.66.0/go.mod h1:1n7X9A0OjJ8CZjeac5PsFxEkIzBioOs223JPLDZ5bbQ= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.13.1 h1:kIfDHRmq8zgw3DB0XxzGTZi19BX7OwIn+yDE9BJ2I1g= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.13.1/go.mod h1:o6YX/HIkSNfumuKUum+FrqrsebMFVJUiGcmGp03WnsQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 h1:RmmWQPREQdk9U+PfqeHW3MqZaBaNK7TpV9W3RY+b+7g= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1/go.mod h1:0A3W4F+68ZnNk5XcNL/e9HFMwnP8RlEicFfy6eOEDyw= +github.com/aws/aws-sdk-go-v2/service/signin v1.8.0 h1:bSvKIoLuRGFqGwASgeCQncCJDi9YKKBDEmCEZzOX1uU= +github.com/aws/aws-sdk-go-v2/service/signin v1.8.0/go.mod h1:9IqUlsJDbUPcg6cgx3WEzXdjrbWzLDQrak0aaSqlTcI= +github.com/aws/aws-sdk-go-v2/service/sso v1.36.0 h1:iivsh357VnfIc18IFWSuoyQEluf8frfWf4cL2Y0JUQw= +github.com/aws/aws-sdk-go-v2/service/sso v1.36.0/go.mod h1:tWuiVBUtPBr8/rgRiYS8Uf85sHcAN+G7XS3D3CEoUh8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.41.0 h1:wVxM3QzSKIK8tSN6OGgezp9OK91lCLH2zhmRInN9rFM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.41.0/go.mod h1:naFe83jSMuYkH+QjQPX8n1MLhBkeCFM5Lsnh5m5wz3c= +github.com/aws/aws-sdk-go-v2/service/sts v1.48.0 h1:RzZVCzYM19vhJCT5s6vO2wN8ie770Li/TmbAZ9B6N7E= +github.com/aws/aws-sdk-go-v2/service/sts v1.48.0/go.mod h1:mKo/CzaCz8qytGW70NG4vIIGAx1HXTlb5lHNkC5k3lk= +github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ= +github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b h1:SHlYZ/bMx7frnmeqCu+xm0TCxXLzX3jQIVuFbnFGtFU= +github.com/cockroachdb/crlib v0.0.0-20241112164430-1264a2edc35b/go.mod h1:Gq51ZeKaFCXk6QwuGM0w1dnaOqc/F5zKT2zA9D6Xeac= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/pebble/v2 v2.1.4 h1:j9wPgMDbkErFdAKYFGhsoCcvzcjR+6zrJ4jhKtJ6bOk= +github.com/cockroachdb/pebble/v2 v2.1.4/go.mod h1:Reo1RTniv1UjVTAu/Fv74y5i3kJ5gmVrPhO9UtFiKn8= +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/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8XTuY0PT9Ane9qZGul/p67vGYwl9BFI= +github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg= +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/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= +github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.8 h1:oQ48q/TMe2SKU8qBE3N7e4/HlG3EpJftom6EsPQgJ58= +github.com/ethereum/c-kzg-4844/v2 v2.1.8/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.17.5 h1:o9BIXs2Q/3cPHVxw49n+Zjn2i6rB9TOXatev46duOC4= +github.com/ethereum/go-ethereum v1.17.5/go.mod h1:vz2YvG7RewA4sFHTgzLyW+WmFG1N4jfk/hgXQVhhn9c= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 h1:h7SZVboCO1iPDtdxPJjs1fj4IVFq/dpDWFevkXtwgps= +github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390/go.mod h1:ur5fRrG8v9twwwRFdX7lquGza4liT6x/xAFW7T4XVIc= +github.com/fil-forge/forgectl v0.0.0-20260507183024-793714651bc2 h1:bmpfrIGcHiqcCCC5U69wRc4+ycraP0DPmw+Vdk6feR4= +github.com/fil-forge/forgectl v0.0.0-20260507183024-793714651bc2/go.mod h1:r3mJDZrMIpgURUjXuD+bb+V8NCUjLQJLCAUsuJo6N2I= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +github.com/filecoin-project/go-data-segment v0.0.1 h1:1wmDxOG4ubWQm3ZC1XI5nCon5qgSq7Ra3Rb6Dbu10Gs= +github.com/filecoin-project/go-data-segment v0.0.1/go.mod h1:H0/NKbsRxmRFBcLibmABv+yFNHdmtl5AyplYLnb0Zv4= +github.com/filecoin-project/go-fil-commcid v0.3.1 h1:4EfxpHSlvtkOqa9weG2Yt5kxFmPib2xU7Uc9Lbqk7fs= +github.com/filecoin-project/go-fil-commcid v0.3.1/go.mod h1:z7Ssf8d7kspF9QRAVHDbZ+43JK4mkhbGH5lyph1TnKY= +github.com/fjl/jsonw v0.1.0 h1:V3MyR79fjLpn/+bMgvegdGUIhoJOzjmqWcKDgcOmY1I= +github.com/fjl/jsonw v0.1.0/go.mod h1:2KMLevM6FXEJnfhtk7naXu9vZdVfOma1GlnGdPRlumU= +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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93 h1:GpQQr4L8jsBtJSURCDqQboOdgpVMU6vR9REjc8nR4Qc= +github.com/golang/snappy v1.0.1-0.20260716114414-9ae09f520e93/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +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.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= +github.com/ipfs/go-log/v2 v2.9.2 h1:O/5BB0elpkRILvT24rCJ5976wWd7u0nJ436T3rdYdc4= +github.com/ipfs/go-log/v2 v2.9.2/go.mod h1:RziRwwXWhndlk8L75RnEe0zeAYaq2heKtEMc3jqUov0= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/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.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882 h1:0lgqHvJWHLGW5TuObJrfyEi6+ASTKDBWikGvPqy9Yiw= +github.com/minio/minlz v1.0.1-0.20250507153514-87eb42fe8882/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +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/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/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/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +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-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun/v3 v3.1.2 h1:86IhD8wFn6IDW4b1/0QzoQS+f5PeA8OHHRn8UZW5ErY= +github.com/pion/stun/v3 v3.1.2/go.mod h1:H7gDic7nNwlUL05pbs6T1dtaBehh/KjupxfWw3ZI7cA= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +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/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +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.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +pitr.ca/jsontokenizer v0.3.2 h1:kzsKwfkWPV5XCmAF//hznh3L9IXbiL8aHbI0FswRkCs= +pitr.ca/jsontokenizer v0.3.2/go.mod h1:3DJdA2QNOU6cI0XkH6pRKZ4Oe8G5SDRUQ6PFAwaQ3YY= diff --git a/delegator/internal/config/config.go b/delegator/internal/config/config.go new file mode 100644 index 00000000..0cfbb1b2 --- /dev/null +++ b/delegator/internal/config/config.go @@ -0,0 +1,204 @@ +package config + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/spf13/viper" + "go.uber.org/fx" +) + +func SupplyConfigs(cfg *Config) fx.Option { + return fx.Supply( + cfg, + cfg.Server, + cfg.Store, + cfg.Delegator, + cfg.Contract, + cfg.Contract.Transactor, + ) +} + +func NewConfig() (*Config, error) { + cfg := &Config{ + Server: ServerConfig{ + Host: viper.GetString("server.host"), + Port: viper.GetInt("server.port"), + }, + Store: DynamoConfig{ + Region: viper.GetString("store.region"), + AllowListTableName: viper.GetString("store.allowlist_table_name"), + ProviderInfoTableName: viper.GetString("store.providerinfo_table_name"), + ProviderWeight: viper.GetUint("store.providerweight"), + Endpoint: viper.GetString("store.endpoint"), + }, + Delegator: DelegatorServiceConfig{ + Key: viper.GetString("delegator.key"), + KeyFile: viper.GetString("delegator.key_file"), + DID: viper.GetString("delegator.did"), + IndexingServiceWebDID: viper.GetString("delegator.indexing_service_web_did"), + IndexingServiceProof: viper.GetString("delegator.indexing_service_proof"), + IndexingServiceProofFile: viper.GetString("delegator.indexing_service_proof_file"), + EgressTrackingServiceDID: viper.GetString("delegator.egress_tracking_service_did"), + EgressTrackingServiceProof: viper.GetString("delegator.egress_tracking_service_proof"), + EgressTrackingServiceProofFile: viper.GetString("delegator.egress_tracking_service_proof_file"), + UploadServiceDID: viper.GetString("delegator.upload_service_did"), + }, + Contract: ContractOperatorConfig{ + ChainClientEndpoint: viper.GetString("contract.chain_client_endpoint"), + PaymentsContractAddress: viper.GetString("contract.payments_contract_address"), + ServiceContractAddress: viper.GetString("contract.service_contract_address"), + RegistryContractAddress: viper.GetString("contract.registry_contract_address"), + Transactor: ContractTransactorConfig{ + ChainID: viper.GetInt64("contract.transactor.chain_id"), + Key: viper.GetString("contract.transactor.key"), + KeystorePath: viper.GetString("contract.transactor.keystore_path"), + KeystorePassword: viper.GetString("contract.transactor.keystore_password"), + }, + }, + } + + if cfg.Server.Host == "" { + cfg.Server.Host = "0.0.0.0" + } + if cfg.Server.Port == 0 { + cfg.Server.Port = 8080 + } + + if cfg.Store.Region == "" { + return nil, fmt.Errorf("store region not set") + } + if cfg.Store.AllowListTableName == "" { + return nil, fmt.Errorf("store allow list table not set") + } + if cfg.Store.ProviderInfoTableName == "" { + return nil, fmt.Errorf("store provider info table not set") + } + + if cfg.Delegator.Key == "" && cfg.Delegator.KeyFile == "" { + return nil, fmt.Errorf("either delegator key or key file must be set") + } + if cfg.Delegator.Key != "" && cfg.Delegator.KeyFile != "" { + return nil, fmt.Errorf("both delegator key and key file are set, please provide only one") + } + if cfg.Delegator.DID == "" { + return nil, fmt.Errorf("delegator DID not set") + } + if cfg.Delegator.IndexingServiceWebDID == "" { + return nil, fmt.Errorf("delegator indexing service DID not set") + } + if cfg.Delegator.IndexingServiceProof == "" && cfg.Delegator.IndexingServiceProofFile == "" { + return nil, fmt.Errorf("delegator indexing service proof not set (set either indexing_service_proof or indexing_service_proof_file)") + } + if cfg.Delegator.IndexingServiceProof != "" && cfg.Delegator.IndexingServiceProofFile != "" { + return nil, fmt.Errorf("both delegator indexing service proof and proof file are set, please provide only one") + } + if cfg.Delegator.EgressTrackingServiceDID == "" { + return nil, fmt.Errorf("delegator egress tracking service DID not set") + } + if cfg.Delegator.EgressTrackingServiceProof == "" && cfg.Delegator.EgressTrackingServiceProofFile == "" { + return nil, fmt.Errorf("delegator egress tracking service proof not set (set either egress_tracking_service_proof or egress_tracking_service_proof_file)") + } + if cfg.Delegator.EgressTrackingServiceProof != "" && cfg.Delegator.EgressTrackingServiceProofFile != "" { + return nil, fmt.Errorf("both delegator egress tracking service proof and proof file are set, please provide only one") + } + if cfg.Delegator.UploadServiceDID == "" { + return nil, fmt.Errorf("delegator upload service DID not set") + } + + if !common.IsHexAddress(cfg.Contract.RegistryContractAddress) { + return nil, fmt.Errorf("registry contract address invalid") + } + if !common.IsHexAddress(cfg.Contract.PaymentsContractAddress) { + return nil, fmt.Errorf("payments contract address invalid") + } + if !common.IsHexAddress(cfg.Contract.ServiceContractAddress) { + return nil, fmt.Errorf("service contract address invalid") + } + if cfg.Contract.ChainClientEndpoint == "" { + return nil, fmt.Errorf("chain client endpoint not set") + } + // TODO we can have stronger validation here, there are only two allowed values + // the filecoin mainnet and calibnet ID + if cfg.Contract.Transactor.ChainID == 0 { + return nil, fmt.Errorf("chain client id not set") + } + // decision on doing this will be made here: https://www.notion.so/storacha/Storacha-Forge-Contract-Billing-Operations-Design-Questions-28b5305b552480d08ea7c8a1ff077a2d?source=copy_link#28f5305b5524801e95e9f556ca7d8a9e + // TODO this is really insecure, we may want to import the aws secret manager sdk and use directly + if cfg.Contract.Transactor.Key == "" { + if cfg.Contract.Transactor.KeystorePath == "" { + return nil, fmt.Errorf("either a transactor key or a keystore must be provided") + } + // TODO this is really insecure, we may want to import the aws secret manager sdk and use directly + if cfg.Contract.Transactor.KeystorePassword == "" { + return nil, fmt.Errorf("transactor keystore password not set") + } + } + if cfg.Contract.Transactor.Key != "" && cfg.Contract.Transactor.KeystorePath != "" { + return nil, fmt.Errorf("both transactor key and keystore are set, only one can be provided") + } + return cfg, nil +} + +type Config struct { + Server ServerConfig `mapstructure:"server"` + Store DynamoConfig `mapstructure:"store"` + Delegator DelegatorServiceConfig `mapstructure:"delegator"` + Contract ContractOperatorConfig `mapstructure:"contract"` +} +type ServerConfig struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` +} + +func (c *ServerConfig) Address() string { + return fmt.Sprintf("%s:%d", c.Host, c.Port) +} + +type DynamoConfig struct { + // Region of the dynamoDB instance + Region string `mapstructure:"region"` + // Name of table we use for allowing users to register + AllowListTableName string `mapstructure:"allowlist_table_name"` + // Name of table we persist registered user data to + ProviderInfoTableName string `mapstructure:"providerinfo_table_name"` + + // ProviderWeight is the weight that will be assigned to a provider + // when they are registered. This value will affect their odds of being + // selected for an upload. `0` means they will not be selected. + ProviderWeight uint `mapstructure:"providerweight"` + + // Endpoint may be set for local testing, usually with docker, e.g. + // docker run -p 8000:8000 amazon/dynamodb-local -jar DynamoDBLocal.jar -sharedDb + // then set endpoint to localhost:8080 + // Do not set for production. + Endpoint string `mapstructure:"endpoint"` // for development +} + +type DelegatorServiceConfig struct { + Key string `mapstructure:"key"` + KeyFile string `mapstructure:"key_file"` + DID string `mapstructure:"did"` + IndexingServiceWebDID string `mapstructure:"indexing_service_web_did"` + IndexingServiceProof string `mapstructure:"indexing_service_proof"` + IndexingServiceProofFile string `mapstructure:"indexing_service_proof_file"` + EgressTrackingServiceDID string `mapstructure:"egress_tracking_service_did"` + EgressTrackingServiceProof string `mapstructure:"egress_tracking_service_proof"` + EgressTrackingServiceProofFile string `mapstructure:"egress_tracking_service_proof_file"` + UploadServiceDID string `mapstructure:"upload_service_did"` +} + +type ContractOperatorConfig struct { + ChainClientEndpoint string `mapstructure:"chain_client_endpoint"` + PaymentsContractAddress string `mapstructure:"payments_contract_address"` + ServiceContractAddress string `mapstructure:"service_contract_address"` + RegistryContractAddress string `mapstructure:"registry_contract_address"` + Transactor ContractTransactorConfig `mapstructure:"transactor"` +} + +type ContractTransactorConfig struct { + ChainID int64 `mapstructure:"chain_id"` + Key string `mapstructure:"key"` + KeystorePath string `mapstructure:"keystore_path"` + KeystorePassword string `mapstructure:"keystore_password"` +} diff --git a/delegator/internal/handlers/handlers.go b/delegator/internal/handlers/handlers.go new file mode 100644 index 00000000..7fe6b5e0 --- /dev/null +++ b/delegator/internal/handlers/handlers.go @@ -0,0 +1,273 @@ +package handlers + +import ( + "errors" + "net/http" + "net/url" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/fil-forge/forge/delegator/internal/services/registrar" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/labstack/echo/v4" +) + +type Handlers struct { + id identity.Identity + service *registrar.Service +} + +func NewHandlers(svcID identity.Identity, svc *registrar.Service) *Handlers { + return &Handlers{ + id: svcID, + service: svc, + } +} + +func (h *Handlers) HealthCheck(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{ + "status": "healthy", + }) +} + +func (h *Handlers) Root(c echo.Context) error { + return c.String(http.StatusOK, "hello") +} + +func (h *Handlers) DIDDocument(c echo.Context) error { + doc, err := h.id.DIDDocument() + if err != nil { + log.Error("failed to get DID document", "error", err) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to get DID document", + }) + } + return c.JSON(http.StatusOK, doc) +} + +type RegisterRequest struct { + Operator string `json:"operator"` + OwnerAddress string `json:"owner_address"` + ProofSetID uint64 `json:"proof_set_id"` + OperatorEmail string `json:"operator_email"` + PublicURL string `json:"public_url"` + // Proofs is a UCAN container delegating the upload service the capabilities + // required of a storage provider. + Proofs string `json:"proofs"` +} + +func (h *Handlers) Register(c echo.Context) error { + var req RegisterRequest + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + } + + // parse and validate request + operator, err := did.Parse(req.Operator) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid DID"}) + } + if !common.IsHexAddress(req.OwnerAddress) { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid owner address"}) + } + endpoint, err := url.Parse(req.PublicURL) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid public URL"}) + } + proofs, err := container.Decode([]byte(req.Proofs)) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid proofs"}) + } + + if err := h.service.Register(c.Request().Context(), registrar.RegisterParams{ + DID: operator, + OwnerAddress: common.HexToAddress(req.OwnerAddress), + ProofSetID: req.ProofSetID, + OperatorEmail: req.OperatorEmail, + PublicURL: *endpoint, + Proofs: proofs, + }); err != nil { + var status int + var message string + switch { + case errors.Is(err, registrar.ErrContractProviderNotRegistered): + status = http.StatusUnprocessableEntity + message = "provider not registered with smart-contract, must register first" + case errors.Is(err, registrar.ErrDIDNotAllowed): + status = http.StatusForbidden + message = "DID not allowed to register, contact Storacha team for help registering" + case errors.Is(err, registrar.ErrDIDAlreadyRegistered): + status = http.StatusConflict + message = "DID already registered" + case errors.Is(err, registrar.ErrBadEndpoint): + status = http.StatusBadRequest + message = "invalid public URL" + case errors.Is(err, registrar.ErrInvalidProof): + status = http.StatusBadRequest + message = "invalid proof" + default: + status = http.StatusInternalServerError + message = err.Error() + } + log.Error("failed to register", "operator", operator, "error", err) + return c.JSON(status, map[string]string{"error": message}) + } + + return c.NoContent(http.StatusCreated) +} + +func (h *Handlers) RequestProof(c echo.Context) error { + return c.String(http.StatusGone, "this endpoint is deprecated, use /registrar/request-proofs instead") +} + +type RequestProofsRequest struct { + DID string `json:"did"` +} + +type RequestProofsResponse struct { + Proofs Proofs `json:"proofs"` +} + +type Proofs struct { + Indexer []byte `json:"indexer"` + EgressTracker []byte `json:"egress_tracker"` +} + +func (h *Handlers) RequestProofs(c echo.Context) error { + var req RequestProofsRequest + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + } + + operator, err := did.Parse(req.DID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid DID"}) + } + + indexerProof, egressTrackerProof, err := h.service.RequestProofs(c.Request().Context(), operator) + if err != nil { + // Map service errors to appropriate HTTP status codes + status := http.StatusInternalServerError + if errors.Is(err, registrar.ErrDIDNotAllowed) || errors.Is(err, registrar.ErrDIDNotRegistered) { + status = http.StatusForbidden + } + + return c.JSON(status, map[string]string{ + "error": err.Error(), + }) + } + + indexerProofStr, err := container.Encode(container.RawGzip, indexerProof) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to read generated indexer proof", + }) + } + + egressTrackerProofStr, err := container.Encode(container.RawGzip, egressTrackerProof) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to read generated egress tracker proof", + }) + } + + return c.JSON(http.StatusOK, RequestProofsResponse{Proofs: Proofs{ + Indexer: indexerProofStr, + EgressTracker: egressTrackerProofStr, + }}) +} + +type IsRegisteredRequest struct { + DID string `json:"did"` +} + +func (h *Handlers) IsRegistered(c echo.Context) error { + var req IsRegisteredRequest + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + } + + operator, err := did.Parse(req.DID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid DID"}) + } + + registered, err := h.service.IsRegisteredDID(c.Request().Context(), operator) + if err != nil { + // TODO map the errors the service returns to http codes + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": err.Error(), + }) + } + + if registered { + return c.NoContent(http.StatusOK) + } + + return c.NoContent(http.StatusNotFound) +} + +// ContractApprovalRequest represents a request to approve a provider for registration +// before full registration with the Storacha network. +type ContractApprovalRequest struct { + // DID is the decentralized identifier of the operator requesting approval + Operator string `json:"operator"` + // OwnerAddress is the Ethereum address of the provider owner (hex format) + OwnerAddress string `json:"owner_address"` + // Signature is the cryptographic signature proving ownership/authorization + Signature []byte `json:"signature"` +} + +// RequestContractApproval handles HTTP requests for contract approval. It validates +// the operator's DID, owner address, and signature, then processes the approval request +// through the registrar service. The provider must already be registered with the +// smart contract and be on the allow list before approval can be granted. +// +// Returns: +// - 204 No Content on success +// - 400 Bad Request for invalid input (DID, owner address, signature, or public URL) +// - 403 Forbidden if the DID is not on the allow list +// - 422 Unprocessable Entity if the provider is not registered with the smart contract +// - 500 Internal Server Error for unexpected errors +func (h *Handlers) RequestContractApproval(c echo.Context) error { + var req ContractApprovalRequest + if err := c.Bind(&req); err != nil { + return c.String(http.StatusBadRequest, "invalid request body") + } + + // parse and validate request + operator, err := did.Parse(req.Operator) + if err != nil { + return c.String(http.StatusBadRequest, "invalid DID") + } + if !common.IsHexAddress(req.OwnerAddress) { + return c.String(http.StatusBadRequest, "invalid OwnerAddress") + } + if len(req.Signature) == 0 { + return c.String(http.StatusBadRequest, "invalid signature") + } + if err := h.service.RequestContractApproval(c.Request().Context(), registrar.RequestApprovalParams{ + Operator: operator, + OwnerAddress: common.HexToAddress(req.OwnerAddress), + Signature: req.Signature, + }); err != nil { + if errors.Is(err, registrar.ErrContractProviderNotRegistered) { + return c.String(http.StatusUnprocessableEntity, "Provider not registered with smart-contract, must register first") + } + if errors.Is(err, registrar.ErrDIDNotAllowed) { + return c.String(http.StatusForbidden, "DID not allowed to register, contact Storacha team for help registering") + } + if errors.Is(err, registrar.ErrInvalidDID) { + return c.String(http.StatusBadRequest, "invalid DID") + } + if errors.Is(err, registrar.ErrInvalidSignature) { + return c.String(http.StatusBadRequest, "signature is invalid") + } + log.Error("failed to request contract approval", "error", err) + return c.NoContent(http.StatusInternalServerError) + } + + return c.NoContent(http.StatusNoContent) +} diff --git a/delegator/internal/providers/providers.go b/delegator/internal/providers/providers.go new file mode 100644 index 00000000..03b2fef0 --- /dev/null +++ b/delegator/internal/providers/providers.go @@ -0,0 +1,218 @@ +package providers + +import ( + "context" + "fmt" + "math/big" + "os" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/fil-forge/forge/delegator/internal/config" + "github.com/fil-forge/forge/delegator/internal/services/registrar" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forgectl/pkg/services/chain" + "github.com/fil-forge/forgectl/pkg/services/inspector" + "github.com/fil-forge/forgectl/pkg/services/operator" + "github.com/fil-forge/forgectl/pkg/services/types" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/delegation" + "go.uber.org/fx" +) + +type SignerParams struct { + fx.In + Config *config.Config +} + +type SignerResult struct { + fx.Out + ID identity.Identity +} + +func ProvideSigner(params SignerParams) (SignerResult, error) { + var id identity.Identity + var err error + switch { + case params.Config.Delegator.Key != "": + id, err = identity.New(params.Config.Delegator.Key, params.Config.Delegator.DID) + if err != nil { + return SignerResult{}, fmt.Errorf("failed to parse multibase key: %w", err) + } + case params.Config.Delegator.KeyFile != "": + id, err = identity.NewFromPEMFileWithDID(params.Config.Delegator.KeyFile, params.Config.Delegator.DID) + if err != nil { + return SignerResult{}, fmt.Errorf("failed to parse key file: %w", err) + } + default: + return SignerResult{}, fmt.Errorf("no key or key file provided") + } + + return SignerResult{ID: id}, nil +} + +type IndexingServiceWebDIDParams struct { + fx.In + Config *config.Config +} + +type IndexingServiceWebDIDResult struct { + fx.Out + IndexingServiceWebDID did.DID `name:"indexing_service_web_did"` +} + +func ProvideIndexingServiceWebDID(params IndexingServiceWebDIDParams) (IndexingServiceWebDIDResult, error) { + parsedDID, err := did.Parse(params.Config.Delegator.IndexingServiceWebDID) + if err != nil { + return IndexingServiceWebDIDResult{}, fmt.Errorf("failed to parse indexing service DID: %w", err) + } + + return IndexingServiceWebDIDResult{IndexingServiceWebDID: parsedDID}, nil +} + +type UploadServiceDIDParams struct { + fx.In + Config *config.Config +} + +type UploadServiceDIDResult struct { + fx.Out + UploadServiceDID did.DID `name:"upload_service_did"` +} + +func ProvideUploadServiceDID(params UploadServiceDIDParams) (UploadServiceDIDResult, error) { + parsedDID, err := did.Parse(params.Config.Delegator.UploadServiceDID) + if err != nil { + return UploadServiceDIDResult{}, fmt.Errorf("failed to parse upload service DID: %w", err) + } + + return UploadServiceDIDResult{UploadServiceDID: parsedDID}, nil +} + +type IndexingServiceProofParams struct { + fx.In + Config *config.Config +} + +type IndexingServiceProofResult struct { + fx.Out + IndexingServiceProof ucan.Delegation `name:"indexing_service_proof"` +} + +func ProvideIndexingServiceProof(params IndexingServiceProofParams) (IndexingServiceProofResult, error) { + var proofStr []byte + + // Prefer proof file over inline proof string + if params.Config.Delegator.IndexingServiceProofFile != "" { + data, err := os.ReadFile(params.Config.Delegator.IndexingServiceProofFile) + if err != nil { + return IndexingServiceProofResult{}, fmt.Errorf("failed to read indexing service proof file: %w", err) + } + proofStr = data + } else { + panic("indexing service proof file must be provided") + //proofStr = params.Config.Delegator.IndexingServiceProof + } + + proof, err := delegation.Decode(proofStr) + if err != nil { + return IndexingServiceProofResult{}, fmt.Errorf("failed to parse indexing service proof: %w", err) + } + + return IndexingServiceProofResult{IndexingServiceProof: proof}, nil +} + +type EgressTrackingServiceDIDParams struct { + fx.In + Config *config.Config +} + +type EgressTrackingServiceDIDResult struct { + fx.Out + EgressTrackingServiceDID did.DID `name:"egress_tracking_service_did"` +} + +func ProvideEgressTrackingServiceDID(params EgressTrackingServiceDIDParams) (EgressTrackingServiceDIDResult, error) { + parsedDID, err := did.Parse(params.Config.Delegator.EgressTrackingServiceDID) + if err != nil { + return EgressTrackingServiceDIDResult{}, fmt.Errorf("failed to parse egress tracking service DID: %w", err) + } + + return EgressTrackingServiceDIDResult{EgressTrackingServiceDID: parsedDID}, nil +} + +type EgressTrackingServiceProofParams struct { + fx.In + Config *config.Config +} + +type EgressTrackingServiceProofResult struct { + fx.Out + EgressTrackingServiceProof ucan.Delegation `name:"egress_tracking_service_proof"` +} + +func ProvideEgressTrackingServiceProof(params EgressTrackingServiceProofParams) (EgressTrackingServiceProofResult, error) { + var proofStr []byte + + // Prefer proof file over inline proof string + if params.Config.Delegator.EgressTrackingServiceProofFile != "" { + data, err := os.ReadFile(params.Config.Delegator.EgressTrackingServiceProofFile) + if err != nil { + return EgressTrackingServiceProofResult{}, fmt.Errorf("failed to read egress tracking service proof file: %w", err) + } + proofStr = data + } else { + panic("egress tracking service proof file must be provided") + //proofStr = params.Config.Delegator.EgressTrackingServiceProof + } + + proof, err := delegation.Decode(proofStr) + if err != nil { + return EgressTrackingServiceProofResult{}, fmt.Errorf("failed to parse egress tracking service proof: %w", err) + } + return EgressTrackingServiceProofResult{EgressTrackingServiceProof: proof}, nil +} + +type SmartContractOperator struct { + o *operator.Service +} + +func (s *SmartContractOperator) IsRegisteredProvider(ctx context.Context, provider common.Address) (bool, error) { + return s.o.RegistryContract.IsRegisteredProvider(&bind.CallOpts{Context: ctx}, provider) +} + +func (s *SmartContractOperator) GetProviderByAddress(ctx context.Context, provider common.Address) (*types.ProviderInfo, error) { + return s.o.GetProviderByAddress(ctx, provider) +} + +func (s *SmartContractOperator) ApproveProvider(ctx context.Context, id uint64) (*types.ApprovalResult, error) { + return s.o.ApproveProvider(ctx, id) +} + +func ProvideContractOperator(cfg config.ContractOperatorConfig) (registrar.ContractOperator, error) { + in, err := inspector.New(inspector.Config{ + ClientEndpoint: cfg.ChainClientEndpoint, + PaymentsContractAddress: common.HexToAddress(cfg.PaymentsContractAddress), + ServiceContractAddress: common.HexToAddress(cfg.ServiceContractAddress), + ProviderRegistryAddress: common.HexToAddress(cfg.RegistryContractAddress), + }) + if err != nil { + return nil, fmt.Errorf("failed to initialize contract inspector: %w", err) + } + txtr, err := chain.NewTransactor(big.NewInt(cfg.Transactor.ChainID), chain.TransactorConfig{ + Key: cfg.Transactor.Key, + KeystorePath: cfg.Transactor.KeystorePath, + KeystorePassword: cfg.Transactor.KeystorePassword, + }) + if err != nil { + return nil, fmt.Errorf("failed to initialize contract transactor: %w", err) + } + + op, err := operator.New(in, txtr) + if err != nil { + return nil, fmt.Errorf("failed to initialize contract operator: %w", err) + } + + return &SmartContractOperator{o: op}, nil +} diff --git a/delegator/internal/server/server.go b/delegator/internal/server/server.go new file mode 100644 index 00000000..5513836c --- /dev/null +++ b/delegator/internal/server/server.go @@ -0,0 +1,69 @@ +package server + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "go.uber.org/fx" + + "github.com/fil-forge/forge/delegator/internal/config" + "github.com/fil-forge/forge/delegator/internal/handlers" +) + +type Server struct { + echo *echo.Echo + config *config.Config + handlers *handlers.Handlers +} + +func NewServer(cfg *config.Config, h *handlers.Handlers) *Server { + e := echo.New() + e.HideBanner = true + + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.RequestID()) + + return &Server{ + echo: e, + config: cfg, + handlers: h, + } +} + +func (s *Server) setupRoutes() { + s.echo.GET("/healthcheck", s.handlers.HealthCheck) + s.echo.GET("/", s.handlers.Root) + s.echo.GET("/.well-known/did.json", s.handlers.DIDDocument) + s.echo.PUT("/registrar/register-node", s.handlers.Register) + s.echo.POST("/registrar/request-approval", s.handlers.RequestContractApproval) + s.echo.GET("/registrar/request-proof", s.handlers.RequestProof) // TODO: deprecated, remove when nobody uses it + s.echo.GET("/registrar/request-proofs", s.handlers.RequestProofs) + s.echo.GET("/registrar/is-registered", s.handlers.IsRegistered) +} + +func Start(lc fx.Lifecycle, s *Server) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + s.setupRoutes() + + go func() { + addr := s.config.Server.Address() + fmt.Printf("Starting server on %s\n", addr) + if err := s.echo.Start(addr); err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Printf("Server error: %v\n", err) + } + }() + + return nil + }, + OnStop: func(ctx context.Context) error { + fmt.Println("Shutting down server...") + return s.echo.Shutdown(ctx) + }, + }) +} diff --git a/delegator/internal/services/registrar/delegator.go b/delegator/internal/services/registrar/delegator.go new file mode 100644 index 00000000..85d187cf --- /dev/null +++ b/delegator/internal/services/registrar/delegator.go @@ -0,0 +1,446 @@ +package registrar + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + logging "github.com/ipfs/go-log/v2" + "go.uber.org/fx" + + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + replicacmds "github.com/fil-forge/forge/protocol/commands/blob/replica" + "github.com/fil-forge/forge/protocol/commands/claim" + pdpcmds "github.com/fil-forge/forge/protocol/commands/pdp" + "github.com/fil-forge/forge/protocol/commands/space/egress" + "github.com/fil-forge/forgectl/pkg/services/types" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/multikey/ed25519/verifier" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/fil-forge/ucantone/ucan/delegation" + ucanlib "github.com/fil-forge/ucantone/ucanlib" + + "github.com/fil-forge/forge/delegator/internal/store" +) + +var log = logging.Logger("service/delegator") + +var ( + // for the unexpected + ErrInternal = errors.New("internal error") + + // we use these to return valid http codes from the handlers using this service + ErrDIDNotAllowed = errors.New("did not is not allowed to register") + ErrDIDAlreadyRegistered = errors.New("did already registered") + ErrDIDNotRegistered = errors.New("did not registered") + ErrBadEndpoint = errors.New("did not found at endpoint") + ErrInvalidProof = errors.New("invalid proof") + ErrInvalidDID = errors.New("invalid did") + ErrInvalidSignature = errors.New("invalid signature") + + // blockchain/smartcontract errors + ErrContractProviderNotRegistered = errors.New("storage provider not registered with registry contract") +) + +type Service struct { + store store.Store + + id identity.Identity + + indexingServiceWebDID did.DID + indexingServiceProof ucan.Delegation + + egressTrackingServiceDID did.DID + egressTrackingServiceProof ucan.Delegation + + uploadServiceDID did.DID + + contractOperator ContractOperator +} + +type ServiceParams struct { + fx.In + + // the store registered providers are persisted to + Store store.Store + + // the identity of the delegator service + ID identity.Identity + + // the web did of the indexing service (TODO is this still required after ./well-known change? + IndexingServiceWebDID did.DID `name:"indexing_service_web_did"` + // proof from the indexer, delegated to this delegator, allowing it to create delegations on behalf of indexing service + IndexingServiceProof ucan.Delegation `name:"indexing_service_proof"` + + // the did of the egress tracking service + EgressTrackingServiceDID did.DID `name:"egress_tracking_service_did"` + // proof from the egress tracking service, delegated to this delegator + EgressTrackingServiceProof ucan.Delegation `name:"egress_tracking_service_proof"` + + // the did of the upload service, used for validating operator proofs are correct + UploadServiceDID did.DID `name:"upload_service_did"` + + ContractOperator ContractOperator +} + +func New(p ServiceParams) *Service { + return &Service{ + store: p.Store, + id: p.ID, + indexingServiceWebDID: p.IndexingServiceWebDID, + indexingServiceProof: p.IndexingServiceProof, + egressTrackingServiceDID: p.EgressTrackingServiceDID, + egressTrackingServiceProof: p.EgressTrackingServiceProof, + uploadServiceDID: p.UploadServiceDID, + contractOperator: p.ContractOperator, + } +} + +// requiredProofs are the capabilities a registering provider must delegate to +// the upload service, identified by command. +var requiredProofs = []ucan.Command{ + blobcmds.Allocate.Command, + blobcmds.Accept.Command, + replicacmds.Allocate.Command, + pdpcmds.Info.Command, +} + +type RegisterParams struct { + DID did.DID + OwnerAddress common.Address + ProofSetID uint64 + OperatorEmail string + PublicURL url.URL + // Proofs delegates the upload service the capabilities in requiredProofs on + // the registering provider (DID). + Proofs ucan.Container +} + +func (s *Service) Register(ctx context.Context, req RegisterParams) error { + // ensure they are allowed to register + allowed, err := s.store.IsAllowedDID(ctx, req.DID) + if err != nil { + return fmt.Errorf("failed to check if DID is allowed: %w", err) + } + if !allowed { + return ErrDIDNotAllowed + } + + // ensure they haven't already registered + registered, err := s.store.IsRegisteredDID(ctx, req.DID) + if err != nil { + return fmt.Errorf("failed to check if DID is registered: %w", err) + } + if registered { + // TODO we may consider allowing this to succeede, if the contract is re-deployed then they need to reregister + // alternative is we are good about clearing dynamo table across redeploys of contract + return ErrDIDAlreadyRegistered + } + + // ensure the proofs delegate every required capability from the provider + // (subject) to the upload service (audience). + proofStore := ucanlib.NewContainerProofStore(req.Proofs) + for _, cmd := range requiredProofs { + chain, _, err := proofStore.ProofChain(ctx, s.uploadServiceDID, cmd, req.DID) + if err != nil { + return fmt.Errorf("%w: building proof chain for %s: %v", ErrInvalidProof, cmd, err) + } + if len(chain) == 0 { + return fmt.Errorf("%w: missing required %s delegation", ErrInvalidProof, cmd) + } + } + + // ensure the did they claim to own is served from the endpoint they claim to own + if valid, err := assertEndpointServesDID(ctx, req.PublicURL, req.DID); err != nil { + log.Errorw("failed to assert endpoint", "DID", req.DID, "error", err) + return err + } else if !valid { + return ErrBadEndpoint + } + + proofs, err := container.Encode(container.Base64Gzip, req.Proofs) + if err != nil { + return fmt.Errorf("encoding proofs: %w", err) + } + + // if we reach here, they have a valid unregistered did in the allow list, with a domain service the did, and valid proof + // so we can create the provider record now. + now := time.Now() + if err := s.store.RegisterProvider(ctx, store.StorageProviderInfo{ + Provider: req.DID.String(), + Endpoint: req.PublicURL.String(), + Address: req.OwnerAddress.String(), + ProofSet: req.ProofSetID, + OperatorEmail: req.OperatorEmail, + Proofs: string(proofs), + InsertedAt: now, + UpdatedAt: now, + }); err != nil { + return fmt.Errorf("failed to register provider: %w", err) + } + // success! + return nil +} + +// TODO add caching for is registered +func (s *Service) IsRegisteredDID(ctx context.Context, operator did.DID) (bool, error) { + // ensure they haven't already registered + registered, err := s.store.IsRegisteredDID(ctx, operator) + if err != nil { + return false, fmt.Errorf("failed to check if DID is registered: %w", err) + } + return registered, nil +} + +func (s *Service) RequestProofs(ctx context.Context, operator did.DID) (ucan.Container, ucan.Container, error) { + // ensure they are allowed to register + allowed, err := s.store.IsAllowedDID(ctx, operator) + if err != nil { + return nil, nil, fmt.Errorf("failed to check if DID is allowed: %w", err) + } + if !allowed { + return nil, nil, ErrDIDNotAllowed + } + + // ensure they haven't already registered + registered, err := s.store.IsRegisteredDID(ctx, operator) + if err != nil { + return nil, nil, fmt.Errorf("failed to check if DID is registered: %w", err) + } + // must be registered to request a proof + if !registered { + return nil, nil, ErrDIDNotRegistered + } + + // the node is in allow list, and already registered, they may haz proof + indexerProof, err := s.generateIndexerDelegation(operator) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate indexer delegation: %w", err) + } + + egressTrackerProof, err := s.generateEgressTrackerDelegation(operator) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate egress tracker delegation: %w", err) + } + + return indexerProof, egressTrackerProof, nil +} + +// generateIndexerDelegation mints the next hop on the chain rooted at +// s.indexingServiceProof (indexing → delegator), extending it to the storage +// node. Subject stays as the indexing service so the chain resolves at +// invocation time: indexing → delegator → storage node. The returned container +// carries both delegations so the node can pass them as proofs when it later +// invokes /claim/cache. +func (s *Service) generateIndexerDelegation(id did.DID) (ucan.Container, error) { + isd, err := claim.Cache.Delegate( + s.id, + id, + s.indexingServiceWebDID, + delegation.WithNoExpiration(), + ) + if err != nil { + return nil, fmt.Errorf("failed to generate delegation from indexing service to storage node: %w", err) + } + return container.New( + container.WithDelegations(s.indexingServiceProof, isd), + ), nil +} + +// generateEgressTrackerDelegation mints the next hop on the chain rooted at +// s.egressTrackingServiceProof (egress → delegator), extending it to the +// storage node. Subject stays as the egress tracking service so the chain +// resolves at invocation time: egress → delegator → storage node. The returned +// container carries both delegations so the node can pass them as proofs when +// it later invokes /space/egress/track. +func (s *Service) generateEgressTrackerDelegation(id did.DID) (ucan.Container, error) { + etd, err := egress.Track.Delegate( + s.id, + id, + s.egressTrackingServiceDID, + delegation.WithNoExpiration(), + ) + if err != nil { + return nil, fmt.Errorf("failed to generate delegation from egress tracking service to storage node: %w", err) + } + return container.New( + container.WithDelegations(s.egressTrackingServiceProof, etd), + ), nil +} + +func assertEndpointServesDID(ctx context.Context, endpoint url.URL, expectedDID did.DID) (bool, error) { + // Create HTTP client with reasonable timeout + client := &http.Client{ + Timeout: 10 * time.Second, + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return false, fmt.Errorf("creating request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + return false, fmt.Errorf("making request to %s: %w", endpoint.String(), err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("unexpected status code %d from %s", resp.StatusCode, endpoint.String()) + } + + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return false, fmt.Errorf("reading response body: %w", err) + } + + // Parse the response to extract DID + responseText := strings.TrimSpace(string(body)) + + // Try to parse as JSON first (in case of structured response) + var didResponse struct { + DID string `json:"did"` + } + + if err := json.Unmarshal(body, &didResponse); err == nil && didResponse.DID != "" { + // Successfully parsed as JSON + if didResponse.DID != expectedDID.String() { + return false, nil + } + return true, nil + } + + // TODO a dedicated DID endpoint on the storage node would be helpful here. + // Parse plain text response to extract DID + // Expected format: "🔥 storage v0.0.3-d6f3761-dirty\n- https://github.com/storacha/storage\n- did:key:z6MksvRCPWoXvMj8sUzuHiQ4pFkSawkKRz2eh1TALNEG6s3e" + lines := strings.Split(responseText, "\n") + var foundDID string + + for _, line := range lines { + line = strings.TrimSpace(line) + // Look for lines that start with "- did:" or just "did:" + if strings.HasPrefix(line, "- did:") { + foundDID = strings.TrimPrefix(line, "- ") + break + } else if strings.HasPrefix(line, "did:") { + foundDID = line + break + } + } + + if foundDID == "" { + return false, nil + } + + if foundDID != expectedDID.String() { + return false, nil + } + + return true, nil +} + +type ContractOperator interface { + IsRegisteredProvider(ctx context.Context, provider common.Address) (bool, error) + GetProviderByAddress(ctx context.Context, provider common.Address) (*types.ProviderInfo, error) + ApproveProvider(ctx context.Context, id uint64) (*types.ApprovalResult, error) +} + +// RequestApprovalParams contains the parameters required to request contract approval +// for a storage provider in the Storacha network. +type RequestApprovalParams struct { + // DID is the decentralized identifier of the operator requesting approval + Operator did.DID + // OwnerAddress is the Ethereum address of the provider owner on the blockchain + OwnerAddress common.Address + // Signature is the cryptographic signature of the DID signed with the DID's private key, + // used to prove ownership of the DID + Signature []byte +} + +// NB(forrest): this is a temporary solution until a decision here is reached: +// https://www.notion.so/storacha/Storacha-Forge-Contract-Billing-Operations-Design-Questions-28b5305b552480d08ea7c8a1ff077a2d?source=copy_link#28f5305b5524801e95e9f556ca7d8a9e + +// RequestContractApproval processes a contract approval request for a storage provider. +// This method performs several validation steps before approving the provider on the blockchain: +// +// 1. Verifies the DID is in the allow list +// 2. Validates the signature to prove DID ownership (provider signs their own DID with its private key) +// 3. Confirms the provider is registered with the smart contract +// 4. Checks if the provider is already approved to avoid duplicate approval calls +// 5. If not approved, submits an approval transaction to the blockchain +// +// The approval process is NOT idempotent at the contract level - repeated calls to approve +// the same provider will fail. This method handles that by checking approval status first. +// +// Returns: +// - nil on success (provider is approved) +// - ErrDIDNotAllowed if the DID is not in the allow list +// - ErrInvalidDID if the DID format is invalid +// - ErrInvalidSignature if the signature verification fails +// - ErrContractProviderNotRegistered if the provider is not registered with the smart contract +// - ErrInternal for unexpected errors (database failures, contract call failures, etc.) +func (s *Service) RequestContractApproval(ctx context.Context, req RequestApprovalParams) error { + // first check if the DID is in the allow list + allowed, err := s.store.IsAllowedDID(ctx, req.Operator) + if err != nil { + log.Errorw("failed to check if DID is allowed", "DID", req.Operator, "error", err) + return ErrInternal + } + if !allowed { + return ErrDIDNotAllowed + } + + // next validate they own the DID they claim + v, err := verifier.ParseKeyDID(req.Operator.String()) + if err != nil { + return ErrInvalidDID + } + // providers sign their own DID with its private key, here we verify the signature. + if !v.Verify([]byte(req.Operator.String()), req.Signature) { + // logging since this may represent someone doing something nasty! + log.Errorw("failed to verify DID", "DID", req.Operator, "signature", req.Signature) + return ErrInvalidSignature + } + + // check if the provider is registered with the contract, they must have register to be approved. + registered, err := s.contractOperator.IsRegisteredProvider(ctx, req.OwnerAddress) + if err != nil { + log.Errorw("failed to check if provider is registered with contract", "address", req.OwnerAddress, "error", err) + return ErrInternal + } + // if they are not registered bail, cannot be approved till registered + if !registered { + return ErrContractProviderNotRegistered + } + // they are registered, check if they have already been approved. + providerInfo, err := s.contractOperator.GetProviderByAddress(ctx, req.OwnerAddress) + if err != nil { + // failure here is an internal error, so log it + log.Errorw("failed to get provider info", "error", err) + return ErrInternal + } + // contract approval calls are NOT idempotent, repeated calls to approve fail, so only approve if unapproved + if !providerInfo.IsApproved { + // approve the provider + res, err := s.contractOperator.ApproveProvider(ctx, providerInfo.ID) + if err != nil { + // failure here is an internal error, so log it + log.Errorw("failed to approve provider", "error", err) + return ErrInternal + } + // TODO probably don't wanna log the whole receipt, but whatever + log.Infow("provider approved with contract", "providerID", res.ProviderID, "transaction", res.TransactionHash, "receipt", res.Receipt) + } + // the provider is approved if we reach this point, success! + return nil +} diff --git a/delegator/internal/store/dynamo.go b/delegator/internal/store/dynamo.go new file mode 100644 index 00000000..9c5192a5 --- /dev/null +++ b/delegator/internal/store/dynamo.go @@ -0,0 +1,295 @@ +package store + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/fil-forge/ucantone/did" + logging "github.com/ipfs/go-log/v2" + + "github.com/fil-forge/forge/delegator/internal/config" +) + +var log = logging.Logger("store/dynamo") + +// DynamoDB provides storage via AWS DynamoDB +type DynamoDB struct { + db *dynamodb.Client + initialized bool + allowListTableName string + providerInfoTableName string + providerWeight uint +} + +// NewDynamoDBStore creates a new DynamoDB-backed store +func NewDynamoDBStore(config config.DynamoConfig) (*DynamoDB, error) { + ctx := context.Background() + + // Use custom BaseEndpoint if endpoint is specified + var opts []func(*awsconfig.LoadOptions) error + if config.Endpoint != "" { + opts = append(opts, awsconfig.WithBaseEndpoint(config.Endpoint)) + + opts = append(opts, awsconfig.WithCredentialsProvider(credentials.StaticCredentialsProvider{ + Value: aws.Credentials{ + AccessKeyID: "dummy", + SecretAccessKey: "dummy", + }, + })) + } + + // Add region if specified + if config.Region != "" { + opts = append(opts, awsconfig.WithRegion(config.Region)) + } + + // Load AWS configuration + cfg, err := awsconfig.LoadDefaultConfig(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("failed to load AWS config: %w", err) + } + + // Create DynamoDB client + client := dynamodb.NewFromConfig(cfg) + + // Create store + store := &DynamoDB{ + db: client, + initialized: false, + allowListTableName: config.AllowListTableName, + providerInfoTableName: config.ProviderInfoTableName, + providerWeight: config.ProviderWeight, + } + + return store, store.initialize(ctx, config) +} + +// Initialize creates tables if they don't exist +func (d *DynamoDB) initialize(ctx context.Context, cfg config.DynamoConfig) error { + if d.initialized { + return nil + } + + tables := []struct { + name string + keySchema []types.KeySchemaElement + attributes []types.AttributeDefinition + indexes []types.GlobalSecondaryIndex + }{ + { + name: cfg.AllowListTableName, + keySchema: []types.KeySchemaElement{ + { + AttributeName: aws.String("did"), + KeyType: types.KeyTypeHash, + }, + }, + attributes: []types.AttributeDefinition{ + { + AttributeName: aws.String("did"), + AttributeType: types.ScalarAttributeTypeS, + }, + }, + }, + { + name: cfg.ProviderInfoTableName, + keySchema: []types.KeySchemaElement{ + { + AttributeName: aws.String("provider"), + KeyType: types.KeyTypeHash, + }, + }, + attributes: []types.AttributeDefinition{ + { + AttributeName: aws.String("provider"), + AttributeType: types.ScalarAttributeTypeS, + }, + }, + // Note: DynamoDB automatically creates attributes as needed when items are put in the table + // Additional fields for StorageProviderInfo: + // - provider (string, primary key) - already defined above + // - endpoint (string) + // - address (string) + // - proofSet (number) + // - operatorEmail (string) + // - proofs (string) + // - insertedAt (timestamp) + // - updatedAt (timestamp) + }, + } + + for _, table := range tables { + // Check if table exists first + _, err := d.db.DescribeTable(ctx, &dynamodb.DescribeTableInput{ + TableName: aws.String(table.name), + }) + + if err == nil { + log.Infow("Table already exists", "table_name", table.name) + continue + // if the endpoint wasn't set it means we are in production mode, and we want to fail if the tables don't already exist + } else if cfg.Endpoint == "" { + return fmt.Errorf("failed to check if table %s exists: %w", table.name, err) + } else { + // else we are in developer mode (endpoint set) so we want to create the tables + input := &dynamodb.CreateTableInput{ + TableName: aws.String(table.name), + KeySchema: table.keySchema, + AttributeDefinitions: table.attributes, + BillingMode: types.BillingModePayPerRequest, // Simpler than provisioned + } + + _, err = d.db.CreateTable(ctx, input) + if err != nil { + return fmt.Errorf("failed to create table %s: %w", table.name, err) + } + } + } + + d.initialized = true + log.Infow("DynamoDB store initialized", + "region", d.db.Options().Region, + "endpoint", d.db.Options().BaseEndpoint) + return nil +} + +// IsAllowedDID checks if a DID is allowed for onboarding (implements PersistentStore interface) +func (d *DynamoDB) IsAllowedDID(ctx context.Context, id did.DID) (bool, error) { + input := &dynamodb.GetItemInput{ + TableName: aws.String(d.allowListTableName), + Key: map[string]types.AttributeValue{ + "did": &types.AttributeValueMemberS{Value: id.String()}, + }, + ProjectionExpression: aws.String("did"), // Only retrieve the key + } + + result, err := d.db.GetItem(ctx, input) + if err != nil { + return false, fmt.Errorf("failed to check DID %s allowlist: %w", id.String(), err) + } + + return len(result.Item) > 0, nil +} + +// AddAllowedDID adds a DID to the allowlist (implements Store interface) +func (d *DynamoDB) AddAllowedDID(ctx context.Context, id did.DID) error { + // Use a simple approach - just add the required key directly + // This avoids any serialization issues with the struct + item := map[string]types.AttributeValue{ + "did": &types.AttributeValueMemberS{Value: id.String()}, + "added_by": &types.AttributeValueMemberS{Value: "system"}, + "added_at": &types.AttributeValueMemberS{Value: time.Now().Format(time.RFC3339)}, + "notes": &types.AttributeValueMemberS{Value: "added by delegator store cli command"}, + } + + input := &dynamodb.PutItemInput{ + TableName: aws.String(d.allowListTableName), + Item: item, + } + + _, err := d.db.PutItem(ctx, input) + if err != nil { + log.Errorw("Error adding DID to allowlist", "did", id.String(), "error", err) + return fmt.Errorf("failed to add DID to allowlist: %w", err) + } + + return nil +} + +// RemoveAllowedDID removes a DID from the allowlist (implements Store interface) +func (d *DynamoDB) RemoveAllowedDID(ctx context.Context, id did.DID) error { + input := &dynamodb.DeleteItemInput{ + TableName: aws.String(d.allowListTableName), + Key: map[string]types.AttributeValue{ + "did": &types.AttributeValueMemberS{Value: id.String()}, + }, + } + + _, err := d.db.DeleteItem(ctx, input) + if err != nil { + log.Errorw("Error removing DID from allowlist", "did", id.String(), "error", err) + return fmt.Errorf("failed to remove DID from allowlist: %w", err) + } + + return nil +} + +// IsRegisteredDID checks if a DID is registered as a provider (implements PersistentStore interface) +func (d *DynamoDB) IsRegisteredDID(ctx context.Context, id did.DID) (bool, error) { + input := &dynamodb.GetItemInput{ + TableName: aws.String(d.providerInfoTableName), + Key: map[string]types.AttributeValue{ + "provider": &types.AttributeValueMemberS{Value: id.String()}, + }, + ProjectionExpression: aws.String("provider"), // Only retrieve the key + } + + result, err := d.db.GetItem(ctx, input) + if err != nil { + return false, fmt.Errorf("failed to check provider registration: %w", err) + } + + return len(result.Item) > 0, nil +} + +// RegisterProvider registers a new provider (implements PersistentStore interface) +func (d *DynamoDB) RegisterProvider(ctx context.Context, info StorageProviderInfo) error { + // Set timestamps if they're not already set + now := time.Now() + info.InsertedAt = now + info.UpdatedAt = now + + // Log info for debugging + log.Infow("Registering provider", + "did", info.Provider, + "endpoint", info.Endpoint) + + // Manually create the item map to ensure all required fields are properly set + item := map[string]types.AttributeValue{ + "provider": &types.AttributeValueMemberS{Value: info.Provider}, + } + + // Add other fields with proper type conversion + if info.Endpoint != "" { + item["endpoint"] = &types.AttributeValueMemberS{Value: info.Endpoint} + } + + if info.Address != "" { + item["address"] = &types.AttributeValueMemberS{Value: info.Address} + } + + // Use string representation for numeric values + item["proofSet"] = &types.AttributeValueMemberN{Value: fmt.Sprintf("%d", info.ProofSet)} + item["weight"] = &types.AttributeValueMemberN{Value: fmt.Sprintf("%d", d.providerWeight)} + + if info.OperatorEmail != "" { + item["operatorEmail"] = &types.AttributeValueMemberS{Value: info.OperatorEmail} + } + + if info.Proofs == "" { + return fmt.Errorf("proofs must be provided") + } + item["proofs"] = &types.AttributeValueMemberS{Value: info.Proofs} + + // Format timestamps as strings + item["insertedAt"] = &types.AttributeValueMemberS{Value: info.InsertedAt.Format(time.RFC3339)} + item["updatedAt"] = &types.AttributeValueMemberS{Value: info.UpdatedAt.Format(time.RFC3339)} + + input := &dynamodb.PutItemInput{ + TableName: aws.String(d.providerInfoTableName), + Item: item, + } + + _, err := d.db.PutItem(ctx, input) + if err != nil { + return fmt.Errorf("failed to register provider: %w", err) + } + + return nil +} diff --git a/delegator/internal/store/types.go b/delegator/internal/store/types.go new file mode 100644 index 00000000..830ebad4 --- /dev/null +++ b/delegator/internal/store/types.go @@ -0,0 +1,37 @@ +package store + +import ( + "context" + "time" + + "github.com/fil-forge/ucantone/did" +) + +// StorageProviderInfo represents the provider metadata table +type StorageProviderInfo struct { + // Provider is the did:key of the storage node. + Provider string `json:"provider" db:"provider"` + // Endpoint is the domain the storage node is reachable at. + Endpoint string `json:"endpoint" db:"endpoint"` + // Address is the ethereum address the storage node uses to submit proofs. + Address string `json:"address" db:"address"` + // ProofSet is the proof set ID the storage node will use. + ProofSet uint64 `json:"proof_set" db:"proof_set"` + // OperatorEmail is the email address of the storage nodes operator. + OperatorEmail string `json:"operator_email" db:"operator_email"` + // Proofs is a UCAN container delegating the upload service the capabilities + // required of this provider. + Proofs string `json:"proofs" db:"proofs"` + // InsertedAt is the time this record was created. + InsertedAt time.Time `json:"inserted_at" db:"inserted_at"` + // UpdatedAt is the time this record was last modified. + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` +} + +type Store interface { + IsAllowedDID(ctx context.Context, did did.DID) (bool, error) + AddAllowedDID(ctx context.Context, did did.DID) error + RemoveAllowedDID(ctx context.Context, did did.DID) error + IsRegisteredDID(ctx context.Context, did did.DID) (bool, error) + RegisterProvider(ctx context.Context, provider StorageProviderInfo) error +} diff --git a/delegator/main.go b/delegator/main.go new file mode 100644 index 00000000..ce290189 --- /dev/null +++ b/delegator/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/fil-forge/forge/delegator/cmd" +) + +func main() { + if err := cmd.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/delegator/test/system_test.go b/delegator/test/system_test.go new file mode 100644 index 00000000..06f79535 --- /dev/null +++ b/delegator/test/system_test.go @@ -0,0 +1,1005 @@ +package test + +import ( + "bytes" + "context" + "fmt" + "net" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/fil-forge/ucantone/multikey" + "github.com/fil-forge/ucantone/multikey/ed25519" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/labstack/echo/v4" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" + + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + replicacmds "github.com/fil-forge/forge/protocol/commands/blob/replica" + "github.com/fil-forge/forge/protocol/commands/claim" + pdpcmds "github.com/fil-forge/forge/protocol/commands/pdp" + "github.com/fil-forge/forge/protocol/commands/space/egress" + forgetypes "github.com/fil-forge/forgectl/pkg/services/types" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/delegation" + + "github.com/fil-forge/forge/delegator/internal/config" + "github.com/fil-forge/forge/delegator/internal/handlers" + "github.com/fil-forge/forge/delegator/internal/server" + "github.com/fil-forge/forge/delegator/internal/services/registrar" + "github.com/fil-forge/forge/delegator/internal/store" + "github.com/fil-forge/forge/internal/client/delegator" +) + +// mockStore implements the store.Store interface for testing +type mockStore struct { + mu sync.RWMutex + allowedDIDs map[string]bool + registeredDIDs map[string]store.StorageProviderInfo +} + +func (m *mockStore) AddAllowedDID(ctx context.Context, did did.DID) error { + m.mu.RLock() + defer m.mu.RUnlock() + m.allowedDIDs[did.String()] = true + return nil +} + +func (m *mockStore) RemoveAllowedDID(ctx context.Context, did did.DID) error { + m.mu.RLock() + defer m.mu.RUnlock() + delete(m.allowedDIDs, did.String()) + return nil +} + +func newMockStore() *mockStore { + return &mockStore{ + allowedDIDs: make(map[string]bool), + registeredDIDs: make(map[string]store.StorageProviderInfo), + } +} + +func (m *mockStore) IsAllowedDID(ctx context.Context, did did.DID) (bool, error) { + m.mu.RLock() + defer m.mu.RUnlock() + return m.allowedDIDs[did.String()], nil +} + +func (m *mockStore) IsRegisteredDID(ctx context.Context, did did.DID) (bool, error) { + m.mu.RLock() + defer m.mu.RUnlock() + _, exists := m.registeredDIDs[did.String()] + return exists, nil +} + +func (m *mockStore) RegisterProvider(ctx context.Context, provider store.StorageProviderInfo) error { + m.mu.Lock() + defer m.mu.Unlock() + m.registeredDIDs[provider.Provider] = provider + return nil +} + +func (m *mockStore) allowDID(did did.DID) { + m.mu.Lock() + defer m.mu.Unlock() + m.allowedDIDs[did.String()] = true +} + +func (m *mockStore) getProvider(did did.DID) (store.StorageProviderInfo, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + provider, exists := m.registeredDIDs[did.String()] + return provider, exists +} + +// Helper functions for test data generation +func generateTestIssuer(t *testing.T) multikey.Issuer { + t.Helper() + issuer, err := ed25519.GenerateIssuer() + if err != nil { + t.Fatalf("failed to create issuer: %v", err) + } + return issuer +} + +func generateIndexingProof(t *testing.T, indexingIssuer ucan.Issuer, delegatorDID did.DID) ucan.Delegation { + dlg, err := claim.Cache.Delegate( + indexingIssuer, + delegatorDID, + indexingIssuer.DID(), + delegation.WithNoExpiration(), + ) + if err != nil { + t.Fatalf("failed to create indexing proof: %v", err) + } + return dlg +} + +func generateEgressTrackingProof(t *testing.T, egressTrackingIssuer ucan.Issuer, delegatorDID did.DID) ucan.Delegation { + dlg, err := egress.Track.Delegate( + egressTrackingIssuer, + delegatorDID, + egressTrackingIssuer.DID(), + delegation.WithNoExpiration(), + ) + if err != nil { + t.Fatalf("failed to create indexing proof: %v", err) + } + return dlg +} + +// providerProofs builds the container a registering provider must supply: the +// provider delegates the required capabilities to the upload service. It is +// returned as a container.Base64Gzip-encoded string, as the register request +// expects. +func providerProofs(t *testing.T, provider ucan.Issuer, uploadServiceDID did.DID) string { + t.Helper() + cmds := []ucan.Command{ + blobcmds.Allocate.Command, + blobcmds.Accept.Command, + replicacmds.Allocate.Command, + pdpcmds.Info.Command, + } + dlgs := make([]ucan.Delegation, 0, len(cmds)) + for _, cmd := range cmds { + dlg, err := delegation.Delegate(provider, uploadServiceDID, provider.DID(), cmd, delegation.WithNoExpiration()) + if err != nil { + t.Fatalf("failed to create %s delegation: %v", cmd, err) + } + dlgs = append(dlgs, dlg) + } + proofs, err := container.Encode(container.Base64Gzip, container.New(container.WithDelegations(dlgs...))) + if err != nil { + t.Fatalf("failed to encode provider proofs: %v", err) + } + return string(proofs) +} + +// findDelegationByIssuer returns the first delegation in ct whose issuer matches +// the given DID. Containers returned by the delegator hold both the upstream +// proof and the freshly-minted hop; tests use this to pick the hop the +// delegator just signed. +func findDelegationByIssuer(t *testing.T, ct *container.Container, issuer did.DID) ucan.Delegation { + t.Helper() + for _, d := range ct.Delegations() { + if d.Issuer() == issuer { + return d + } + } + t.Fatalf("no delegation in container issued by %s", issuer) + return nil +} + +// mockStorageNode simulates a storage node server +type mockStorageNode struct { + server *httptest.Server + did did.DID + issuer ucan.Issuer + handler *echo.Echo +} + +func newMockStorageNode(t *testing.T) *mockStorageNode { + issuer := generateTestIssuer(t) + e := echo.New() + e.HideBanner = true + + node := &mockStorageNode{ + did: issuer.DID(), + issuer: issuer, + handler: e, + } + + // Mock storage node endpoints + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, fmt.Sprintf("🔥 storage v0.0.3\n- https://github.com/storacha/storage\n- %s", node.did.String())) + }) + + // Mock blob allocate endpoint + e.POST("/allocate", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{ + "url": node.server.URL + "/upload", + "headers": map[string]string{ + "Authorization": "Bearer test-token", + }, + }) + }) + + // Mock blob upload endpoint + e.PUT("/upload", func(c echo.Context) error { + return c.NoContent(http.StatusOK) + }) + + // Mock blob accept endpoint + e.POST("/accept", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{ + "locationCommitment": map[string]interface{}{ + "location": []string{node.server.URL + "/download/test"}, + }, + "pdpAccept": map[string]interface{}{ + "piece": map[string]interface{}{ + "link": "baga6ea4seaqtest", + }, + }, + }) + }) + + // Mock download endpoint + e.GET("/download/:id", func(c echo.Context) error { + // Return some test data + data := make([]byte, 1024) + return c.Blob(http.StatusOK, "application/octet-stream", data) + }) + + node.server = httptest.NewServer(node.handler) + return node +} + +func (n *mockStorageNode) close() { + n.server.Close() +} + +func (n *mockStorageNode) url() string { + return n.server.URL +} + +// Test server setup +func setupTestServer(t *testing.T, mockStore *mockStore) (*fxtest.App, string, ucan.Issuer, ucan.Issuer, ucan.Issuer, ucan.Issuer, *mockContractOperator) { + // Generate test signers + delegatorIssuer := generateTestIssuer(t) + indexingIssuer := generateTestIssuer(t) + egressTrackingIssuer := generateTestIssuer(t) + uploadIssuer := generateTestIssuer(t) + + // Create mock contract operator + mockContractOp := newMockContractOperator() + + // Generate indexing proof + indexingProof := generateIndexingProof(t, indexingIssuer, delegatorIssuer.DID()) + + // Generate egress tracking proof + egressTrackingProof := generateEgressTrackingProof(t, egressTrackingIssuer, delegatorIssuer.DID()) + + // Get a free port + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to get free port: %v", err) + } + port := listener.Addr().(*net.TCPAddr).Port + listener.Close() + + // Create test configuration + testConfig := &config.Config{ + Server: config.ServerConfig{ + Host: "127.0.0.1", + Port: port, + }, + Delegator: config.DelegatorServiceConfig{ + IndexingServiceWebDID: indexingIssuer.DID().String(), + EgressTrackingServiceDID: egressTrackingIssuer.DID().String(), + UploadServiceDID: uploadIssuer.DID().String(), + }, + } + + // Create the test app with fx + app := fxtest.New(t, + fx.Provide( + func() *config.Config { return testConfig }, + func() store.Store { return mockStore }, + func() identity.Identity { return identity.Identity{Issuer: delegatorIssuer} }, + fx.Annotate( + func() (did.DID, error) { + return did.Parse(indexingIssuer.DID().String()) + }, + fx.ResultTags(`name:"indexing_service_web_did"`), + ), + fx.Annotate( + func() (did.DID, error) { + return did.Parse(egressTrackingIssuer.DID().String()) + }, + fx.ResultTags(`name:"egress_tracking_service_did"`), + ), + fx.Annotate( + func() did.DID { return uploadIssuer.DID() }, + fx.ResultTags(`name:"upload_service_did"`), + ), + fx.Annotate( + func() ucan.Delegation { return indexingProof }, + fx.ResultTags(`name:"indexing_service_proof"`), + ), + fx.Annotate( + func() ucan.Delegation { return egressTrackingProof }, + fx.ResultTags(`name:"egress_tracking_service_proof"`), + ), + func() registrar.ContractOperator { return mockContractOp }, + registrar.New, + handlers.NewHandlers, + server.NewServer, + ), + fx.Invoke(server.Start), + ) + + app.RequireStart() + + // Wait for server to be ready + serverURL := fmt.Sprintf("http://127.0.0.1:%d", port) + for i := 0; i < 50; i++ { + resp, err := http.Get(serverURL + "/healthcheck") + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + break + } + time.Sleep(100 * time.Millisecond) + } + + return app, serverURL, delegatorIssuer, indexingIssuer, egressTrackingIssuer, uploadIssuer, mockContractOp +} + +func TestSystemHealthCheck(t *testing.T) { + mockStore := newMockStore() + app, serverURL, _, _, _, _, _ := setupTestServer(t, mockStore) + defer app.RequireStop() + + // Create client + c, err := client.New(serverURL) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + // Test health check + ctx := context.Background() + err = c.HealthCheck(ctx) + if err != nil { + t.Fatalf("health check failed: %v", err) + } +} + +func TestSystemDIDDocument(t *testing.T) { + mockStore := newMockStore() + app, serverURL, delegatorSigner, _, _, _, _ := setupTestServer(t, mockStore) + defer app.RequireStop() + + // Create client + c, err := client.New(serverURL) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + // Test DID document endpoint + ctx := context.Background() + doc, err := c.DIDDocument(ctx) + if err != nil { + t.Fatalf("get did document failed: %v", err) + } + + if doc.ID != delegatorSigner.DID() { + t.Fatalf("unexpected id: got %s, want %s", doc.ID, delegatorSigner.DID().String()) + } +} + +func TestSystemRegistrationFlow(t *testing.T) { + mockStore := newMockStore() + app, serverURL, _, _, _, uploadSigner, _ := setupTestServer(t, mockStore) + defer app.RequireStop() + + // Create client + c, err := client.New(serverURL) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + // Create a mock storage node + storageNode := newMockStorageNode(t) + defer storageNode.close() + + // Allow the storage node DID + mockStore.allowDID(storageNode.did) + + // Test registration + t.Run("successful registration", func(t *testing.T) { + err = c.Register(t.Context(), &client.RegisterRequest{ + Operator: storageNode.did.String(), + OwnerAddress: common.HexToAddress("0x1234567890123456789012345678901234567890").String(), + ProofSetID: 1, + OperatorEmail: "test@example.com", + PublicURL: storageNode.url(), + Proofs: providerProofs(t, storageNode.issuer, uploadSigner.DID()), + }) + if err != nil { + t.Fatalf("registration failed: %v", err) + } + + // Verify registration in store + provider, exists := mockStore.getProvider(storageNode.did) + if !exists { + t.Fatal("provider not found in store after registration") + } + if provider.Provider != storageNode.did.String() { + t.Fatalf("unexpected provider DID: got %s, want %s", provider.Provider, storageNode.did.String()) + } + if provider.Proofs == "" { + t.Fatal("expected provider proofs to be stored") + } + }) + + t.Run("duplicate registration should fail", func(t *testing.T) { + err = c.Register(t.Context(), &client.RegisterRequest{ + Operator: storageNode.did.String(), + OwnerAddress: common.HexToAddress("0x1234567890123456789012345678901234567890").String(), + ProofSetID: 1, + OperatorEmail: "test@example.com", + PublicURL: storageNode.url(), + Proofs: providerProofs(t, storageNode.issuer, uploadSigner.DID()), + }) + if err == nil { + t.Fatal("expected duplicate registration to fail") + } + }) + + t.Run("unauthorized DID registration should fail", func(t *testing.T) { + unauthorizedSigner := generateTestIssuer(t) + unauthorizedNode := newMockStorageNode(t) + defer unauthorizedNode.close() + + err = c.Register(t.Context(), &client.RegisterRequest{ + Operator: unauthorizedSigner.DID().String(), + OwnerAddress: common.HexToAddress("0x1234567890123456789012345678901234567890").String(), + ProofSetID: 1, + OperatorEmail: "test@example.com", + PublicURL: unauthorizedNode.url(), + Proofs: providerProofs(t, unauthorizedSigner, uploadSigner.DID()), + }) + if err == nil { + t.Fatal("expected unauthorized registration to fail") + } + }) + + t.Run("missing proofs registration should fail", func(t *testing.T) { + missingProofsNode := newMockStorageNode(t) + defer missingProofsNode.close() + mockStore.allowDID(missingProofsNode.did) + + err = c.Register(t.Context(), &client.RegisterRequest{ + Operator: missingProofsNode.did.String(), + OwnerAddress: common.HexToAddress("0x1234567890123456789012345678901234567890").String(), + ProofSetID: 1, + OperatorEmail: "test@example.com", + PublicURL: missingProofsNode.url(), + // Proofs intentionally omitted. + }) + if err == nil { + t.Fatal("expected registration without proofs to fail") + } + }) +} + +func TestSystemIsRegistered(t *testing.T) { + mockStore := newMockStore() + app, serverURL, _, _, _, uploadSigner, _ := setupTestServer(t, mockStore) + defer app.RequireStop() + + // Create client + c, err := client.New(serverURL) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + ctx := context.Background() + + // Create and register a storage node + storageNode := newMockStorageNode(t) + defer storageNode.close() + mockStore.allowDID(storageNode.did) + + // Register the node + err = c.Register(ctx, &client.RegisterRequest{ + Operator: storageNode.did.String(), + OwnerAddress: common.HexToAddress("0x1234567890123456789012345678901234567890").String(), + ProofSetID: 1, + OperatorEmail: "test@example.com", + PublicURL: storageNode.url(), + Proofs: providerProofs(t, storageNode.issuer, uploadSigner.DID()), + }) + if err != nil { + t.Fatalf("registration failed: %v", err) + } + + t.Run("check registered DID", func(t *testing.T) { + registered, err := c.IsRegistered(ctx, &client.IsRegisteredRequest{ + DID: storageNode.did.String(), + }) + if err != nil { + t.Fatalf("is registered check failed: %v", err) + } + if !registered { + t.Fatal("expected DID to be registered") + } + }) + + t.Run("check unregistered DID", func(t *testing.T) { + unregisteredSigner := generateTestIssuer(t) + registered, err := c.IsRegistered(ctx, &client.IsRegisteredRequest{ + DID: unregisteredSigner.DID().String(), + }) + if err != nil { + t.Fatalf("is registered check failed: %v", err) + } + if registered { + t.Fatal("expected DID to not be registered") + } + }) +} + +func TestSystemRequestProofs(t *testing.T) { + mockStore := newMockStore() + app, serverURL, delegatorSigner, indexingSigner, egressTrackingSigner, uploadSigner, _ := setupTestServer(t, mockStore) + defer app.RequireStop() + + // Create client + c, err := client.New(serverURL) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + ctx := context.Background() + + // Create and register a storage node + storageNode := newMockStorageNode(t) + defer storageNode.close() + mockStore.allowDID(storageNode.did) + + // Register the node + err = c.Register(ctx, &client.RegisterRequest{ + Operator: storageNode.did.String(), + OwnerAddress: common.HexToAddress("0x1234567890123456789012345678901234567890").String(), + ProofSetID: 1, + OperatorEmail: "test@example.com", + PublicURL: storageNode.url(), + Proofs: providerProofs(t, storageNode.issuer, uploadSigner.DID()), + }) + if err != nil { + t.Fatalf("registration failed: %v", err) + } + + t.Run("request proofs for registered DID", func(t *testing.T) { + resp, err := c.RequestProofs(ctx, storageNode.did.String()) + if err != nil { + t.Fatalf("request proof failed: %v", err) + } + + // Verify indexer proof + if resp.Proofs.Indexer == nil { + t.Fatal("expected indexer proof to be returned") + } + + indxrContainer, err := container.Decode(resp.Proofs.Indexer) + if err != nil { + t.Fatalf("failed to parse returned indexer proof container: %v", err) + } + indxrDlg := findDelegationByIssuer(t, indxrContainer, delegatorSigner.DID()) + + // Verify issuer is the delegator + if indxrDlg.Issuer() != delegatorSigner.DID() { + t.Fatalf("unexpected indexer proof issuer: got %s, want %s", indxrDlg.Issuer(), delegatorSigner.DID()) + } + + // Verify audience is the storage node + if indxrDlg.Audience() != storageNode.did { + t.Fatalf("unexpected indexer proof audience: got %s, want %s", indxrDlg.Audience(), storageNode.did) + } + + // Verify capability + cmd := indxrDlg.Command() + if !cmd.Defined() { + t.Fatalf("indxer proof command not defined") + } + if !cmd.Proves(claim.Cache.Command) { + t.Fatalf("unexpected capability in indexer proof, does not prove %s", claim.Cache) + } + if indxrDlg.Subject() != indexingSigner.DID() { + t.Fatalf("unexpected subject in indexer proof: got %s, want %s", indxrDlg.Subject(), indexingSigner.DID()) + } + + // Verify egress tracker proof + if resp.Proofs.EgressTracker == nil { + t.Fatal("expected egress tracker proof to be returned") + } + + etrackerContainer, err := container.Decode(resp.Proofs.EgressTracker) + if err != nil { + t.Fatalf("failed to parse returned egress tracker proof container: %v", err) + } + etrackerDlg := findDelegationByIssuer(t, etrackerContainer, delegatorSigner.DID()) + + // Verify issuer is the delegator + if etrackerDlg.Issuer() != delegatorSigner.DID() { + t.Fatalf("unexpected egress tracker proof issuer: got %s, want %s", etrackerDlg.Issuer(), delegatorSigner.DID()) + } + + // Verify audience is the storage node + if etrackerDlg.Audience() != storageNode.did { + t.Fatalf("unexpected egress tracker proof audience: got %s, want %s", etrackerDlg.Audience(), storageNode.did) + } + + // Verify capability + cmd = etrackerDlg.Command() + if !cmd.Defined() { + t.Fatalf("etracker proof command not defined") + } + if !cmd.Proves(egress.Track.Command) { + t.Fatalf("unexpected capability in egress tracker proof, does not prove %s", egress.Track) + } + if etrackerDlg.Subject() != egressTrackingSigner.DID() { + t.Fatal("unexpected subject in egress tracker proof") + } + }) + + t.Run("request proof for unregistered DID", func(t *testing.T) { + unregisteredSigner := generateTestIssuer(t) + mockStore.allowDID(unregisteredSigner.DID()) // Allow but don't register + + _, err := c.RequestProofs(ctx, unregisteredSigner.DID().String()) + if err == nil { + t.Fatal("expected request proof to fail for unregistered DID") + } + }) + + t.Run("request proof for unauthorized DID", func(t *testing.T) { + unauthorizedSigner := generateTestIssuer(t) + // Don't allow this DID + + _, err := c.RequestProofs(ctx, unauthorizedSigner.DID().String()) + if err == nil { + t.Fatal("expected request proof to fail for unauthorized DID") + } + }) +} + +func TestSystemEndToEndWorkflow(t *testing.T) { + mockStore := newMockStore() + app, serverURL, delegatorSigner, indexingSigner, egressTrackingSigner, uploadSigner, _ := setupTestServer(t, mockStore) + defer app.RequireStop() + + // Create client + c, err := client.New(serverURL) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + ctx := context.Background() + + // Create a mock storage node + storageNode := newMockStorageNode(t) + defer storageNode.close() + + // Allow the storage node DID + mockStore.allowDID(storageNode.did) + + // Step 1: Check that node is not registered + registered, err := c.IsRegistered(ctx, &client.IsRegisteredRequest{ + DID: storageNode.did.String(), + }) + if err != nil { + t.Fatalf("is registered check failed: %v", err) + } + if registered { + t.Fatal("expected DID to not be registered initially") + } + + // Step 2: Try to request proof before registration (should fail) + _, err = c.RequestProofs(ctx, storageNode.did.String()) + if err == nil { + t.Fatal("expected request proof to fail before registration") + } + + err = c.Register(ctx, &client.RegisterRequest{ + Operator: storageNode.did.String(), + OwnerAddress: common.HexToAddress("0x1234567890123456789012345678901234567890").String(), + ProofSetID: 1, + OperatorEmail: "test@example.com", + PublicURL: storageNode.url(), + Proofs: providerProofs(t, storageNode.issuer, uploadSigner.DID()), + }) + if err != nil { + t.Fatalf("registration failed: %v", err) + } + + // Step 3: Verify node is now registered + registered, err = c.IsRegistered(ctx, &client.IsRegisteredRequest{ + DID: storageNode.did.String(), + }) + if err != nil { + t.Fatalf("is registered check failed: %v", err) + } + if !registered { + t.Fatal("expected DID to be registered after registration") + } + + // Step 4: Request proof after registration (should succeed) + proofResp, err := c.RequestProofs(ctx, storageNode.did.String()) + if err != nil { + t.Fatalf("request proof failed: %v", err) + } + if proofResp.Proofs.Indexer == nil { + t.Fatal("expected indexer proof to be returned") + } + if proofResp.Proofs.EgressTracker == nil { + t.Fatal("expected egress tracker proof to be returned") + } + + // Step 5: Verify the proofs can be parsed and are valid + con, err := container.Decode(proofResp.Proofs.Indexer) + if err != nil { + t.Fatalf("failed to parse indexer proof: %v", err) + } + dlg := findDelegationByIssuer(t, con, delegatorSigner.DID()) + if dlg.Audience() != storageNode.did { + t.Fatalf("indexer proof audience mismatch: got %s, want %s", dlg.Audience(), storageNode.did) + } + if dlg.Subject() != indexingSigner.DID() { + t.Fatalf("indexer proof subject mismatch: got %s, want %s", dlg.Subject(), indexingSigner.DID()) + } + + con, err = container.Decode(proofResp.Proofs.EgressTracker) + if err != nil { + t.Fatalf("failed to parse egress tracker proof: %v", err) + } + dlg = findDelegationByIssuer(t, con, delegatorSigner.DID()) + if dlg.Audience() != storageNode.did { + t.Fatalf("egress tracker proof audience mismatch: got %s, want %s", dlg.Audience(), storageNode.did) + } + if dlg.Subject() != egressTrackingSigner.DID() { + t.Fatalf("egress tracker proof subject mismatch: got %s, want %s", dlg.Subject(), egressTrackingSigner.DID()) + } +} + +func TestSystemInvalidRequests(t *testing.T) { + mockStore := newMockStore() + app, serverURL, _, _, _, _, _ := setupTestServer(t, mockStore) + defer app.RequireStop() + + ctx := context.Background() + + tests := []struct { + name string + method string + endpoint string + body string + wantCode int + }{ + { + name: "malformed JSON in register", + method: "PUT", + endpoint: "/registrar/register-node", + body: `{"invalid json`, + wantCode: http.StatusBadRequest, + }, + { + name: "invalid DID in register", + method: "PUT", + endpoint: "/registrar/register-node", + body: `{"did": "not-a-did", "owner_address": "0x1234567890123456789012345678901234567890", "proof_set_id": 1, "operator_email": "test@example.com", "public_url": "http://example.com", "proof": "test"}`, + wantCode: http.StatusBadRequest, + }, + { + name: "invalid address in register", + method: "PUT", + endpoint: "/registrar/register-node", + body: `{"did": "did:key:z6MksvRCPWoXvMj8sUzuHiQ4pFkSawkKRz2eh1TALNEG6s3e", "owner_address": "not-an-address", "proof_set_id": 1, "operator_email": "test@example.com", "public_url": "http://example.com", "proof": "test"}`, + wantCode: http.StatusBadRequest, + }, + { + name: "malformed JSON in is-registered", + method: "GET", + endpoint: "/registrar/is-registered", + body: `{"invalid json`, + wantCode: http.StatusBadRequest, + }, + { + name: "invalid DID in is-registered", + method: "GET", + endpoint: "/registrar/is-registered", + body: `{"did": "not-a-did"}`, + wantCode: http.StatusBadRequest, + }, + { + name: "deprecated method registrar/request-proof returns 410", + method: "GET", + endpoint: "/registrar/request-proof", + body: `{"did": "did:key:z6MksvRCPWoXvMj8sUzuHiQ4pFkSawkKRz2eh1TALNEG6s3e"}`, + wantCode: http.StatusGone, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequestWithContext(ctx, tt.method, serverURL+tt.endpoint, bytes.NewReader([]byte(tt.body))) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != tt.wantCode { + t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, tt.wantCode) + } + }) + } +} + +// mockContractOperator implements the registrar.ContractOperator interface for testing +type mockContractOperator struct { + mu sync.RWMutex + registeredProviders map[string]*forgetypes.ProviderInfo + approvedProviders map[uint64]bool + nextProviderID uint64 +} + +func newMockContractOperator() *mockContractOperator { + return &mockContractOperator{ + registeredProviders: make(map[string]*forgetypes.ProviderInfo), + approvedProviders: make(map[uint64]bool), + nextProviderID: 1, + } +} + +func (m *mockContractOperator) IsRegisteredProvider(ctx context.Context, provider common.Address) (bool, error) { + m.mu.RLock() + defer m.mu.RUnlock() + _, exists := m.registeredProviders[provider.String()] + return exists, nil +} + +func (m *mockContractOperator) GetProviderByAddress(ctx context.Context, provider common.Address) (*forgetypes.ProviderInfo, error) { + m.mu.RLock() + defer m.mu.RUnlock() + info, exists := m.registeredProviders[provider.String()] + if !exists { + return nil, fmt.Errorf("provider not found") + } + return info, nil +} + +func (m *mockContractOperator) ApproveProvider(ctx context.Context, id uint64) (*forgetypes.ApprovalResult, error) { + m.mu.Lock() + defer m.mu.Unlock() + m.approvedProviders[id] = true + return &forgetypes.ApprovalResult{ + ProviderID: id, + TransactionHash: common.HexToHash("0x1234567890abcdef"), + }, nil +} + +func (m *mockContractOperator) registerProvider(address common.Address, isApproved bool) { + m.mu.Lock() + defer m.mu.Unlock() + id := m.nextProviderID + m.nextProviderID++ + m.registeredProviders[address.String()] = &forgetypes.ProviderInfo{ + ID: id, + IsApproved: isApproved, + } +} + +func TestSystemRequestContractApproval(t *testing.T) { + mockStore := newMockStore() + app, serverURL, _, _, _, _, mockContractOp := setupTestServer(t, mockStore) + defer app.RequireStop() + + // Create client + c, err := client.New(serverURL) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + ctx := context.Background() + + // Test provider address + testAddress := common.HexToAddress("0x1234567890123456789012345678901234567890") + + t.Run("successful contract approval", func(t *testing.T) { + // Create a test signer + signer := generateTestIssuer(t) + mockStore.allowDID(signer.DID()) + mockContractOp.registerProvider(testAddress, false) // Register but not yet approved + + // Sign the DID with the signer's private key to prove ownership + signature := signer.Sign([]byte(signer.DID().String())) + + err = c.RequestApproval(ctx, &client.RequestApprovalRequest{ + Operator: signer.DID().String(), + OwnerAddress: testAddress.String(), + Signature: signature, + }) + if err != nil { + t.Fatalf("contract approval failed: %v", err) + } + }) + + t.Run("DID not in allow list", func(t *testing.T) { + // Create a test signer but don't add to allow list + signer := generateTestIssuer(t) + testAddr := common.HexToAddress("0x2234567890123456789012345678901234567890") + + signature := signer.Sign([]byte(signer.DID().String())) + + err = c.RequestApproval(ctx, &client.RequestApprovalRequest{ + Operator: signer.DID().String(), + OwnerAddress: testAddr.String(), + Signature: signature, + }) + if err == nil { + t.Fatal("expected contract approval to fail for DID not in allow list") + } + }) + + t.Run("invalid signature", func(t *testing.T) { + // Create a test signer and add to allow list + signer := generateTestIssuer(t) + mockStore.allowDID(signer.DID()) + testAddr := common.HexToAddress("0x3234567890123456789012345678901234567890") + mockContractOp.registerProvider(testAddr, false) + + // Use an invalid signature + invalidSignature := make([]byte, 64) + + err = c.RequestApproval(ctx, &client.RequestApprovalRequest{ + Operator: signer.DID().String(), + OwnerAddress: testAddr.String(), + Signature: invalidSignature, + }) + if err == nil { + t.Fatal("expected contract approval to fail with invalid signature") + } + }) + + t.Run("provider not registered with contract", func(t *testing.T) { + // Create a test signer and add to allow list but don't register with contract + signer := generateTestIssuer(t) + mockStore.allowDID(signer.DID()) + testAddr := common.HexToAddress("0x4234567890123456789012345678901234567890") + // Do NOT register with contract + + signature := signer.Sign([]byte(signer.DID().String())) + + err = c.RequestApproval(ctx, &client.RequestApprovalRequest{ + Operator: signer.DID().String(), + OwnerAddress: testAddr.String(), + Signature: signature, + }) + if err == nil { + t.Fatal("expected contract approval to fail for provider not registered with contract") + } + }) + + t.Run("already approved provider (idempotent)", func(t *testing.T) { + // Create a test signer + signer := generateTestIssuer(t) + mockStore.allowDID(signer.DID()) + testAddr := common.HexToAddress("0x5234567890123456789012345678901234567890") + mockContractOp.registerProvider(testAddr, true) // Already approved + + signature := signer.Sign([]byte(signer.DID().String())) + + // Should succeed even if already approved (idempotent behavior) + err = c.RequestApproval(ctx, &client.RequestApprovalRequest{ + Operator: signer.DID().String(), + OwnerAddress: testAddr.String(), + Signature: signature, + }) + if err != nil { + t.Fatalf("contract approval failed for already approved provider: %v", err) + } + }) +} diff --git a/delegator/version.json b/delegator/version.json new file mode 100644 index 00000000..cd2be808 --- /dev/null +++ b/delegator/version.json @@ -0,0 +1,3 @@ +{ + "version": "v0.1.1" +} diff --git a/docker/Dockerfile b/docker/Dockerfile index 06d7909d..d0e98c94 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,15 +1,23 @@ -# The one service image. Every in-repo Go service (piri, hilt, sprue, ingot) -# builds from this file — there are deliberately no per-service Dockerfiles. -# The services are structurally identical (a single static Go binary listening -# on ports the compose files map), so their images are one parameterized build: +# The one service image. Every in-repo Go service (piri, hilt, sprue, ingot, +# delegator, piri-signing-service, indexing-service) builds from this file — +# there are deliberately no per-service Dockerfiles. The services are +# structurally identical (a single static Go binary listening on ports the +# compose files map), so their images are one parameterized build: # # docker build -f docker/Dockerfile --build-arg SERVICE= \ -# --target {prod|dev} / +# --target {prod|dev} . # -# BUILD CONTEXT IS THE SERVICE DIRECTORY — each service stays a self-contained -# context (scoped by its own .dockerignore), and a change to one service can -# never invalidate another's layer cache. The repo-root go.work is outside -# every context, keeping builds hermetic on each service's go.mod. +# BUILD CONTEXT IS THE REPOSITORY ROOT. Every service reaches the shared +# modules (protocol/, internal/, attestation/) — and some reach their siblings +# (hilt uses smelt's stack harness in its integration tests, ingot uses the +# indexer's query client) — through replace directives in its go.mod, +# and a replace target outside the build context cannot be seen, so the +# context is the whole tree. The root .dockerignore keeps it small. Dependency +# downloads are cached on the manifests layer (every module's go.mod/go.sum, +# copied before any source) and compilation on the go-build cache mount, so a +# rebuild after a one-file change is still incremental even though the source +# COPY covers the tree. go.work is ignored, so each build is hermetic on its +# module's go.mod. # # Targets: # build deps + source, native toolchain cross-compiling to TARGETARCH @@ -17,32 +25,52 @@ # build-dev debuggable binary (-N -l: no optimization/inlining) + delve # prod minimal debian runtime + ca-certificates, curl and wget (the # compose healthchecks use curl; smelt's generated piri -# healthcheck uses wget — both ship so no compose file cares) +# healthcheck uses wget — both ship so no compose file cares). +# The wget is GNU wget: its --spider probes with HEAD, where +# busybox's probes with GET, so a compose probe against a +# GET-only route must use `wget -O /dev/null` instead # dev prod + a network/debug toolset and the dlv debugger; published # as :main-dev for ad-hoc engineer debugging # # Build args: -# SERVICE (required) the service name; names the binary at /usr/bin/ -# MAIN_PKG main package path (default ./cmd; ingot passes ./cmd/ingot) +# SERVICE (required) the service directory; also names the image +# MAIN_PKG main package path within the service (default ./cmd; ingot +# passes ./cmd/ingot, delegator and piri-signing-service pass .) +# BIN binary name inside the image (default: SERVICE). The +# satellites keep the names their compose files and smelt's +# workspace table expect: delegator -> registrar, +# piri-signing-service -> signer, indexing-service -> indexer # BUILD_TAGS go build tags (piri passes "skiff" — selects Curio's FFI-free # variants, which is what makes its CGO_ENABLED=0 build possible) # # ENTRYPOINT is the bare binary for every service: the compose `command:` # supplies the subcommand (e.g. serve --config …), and the dev image can be # launched under dlv by overriding `command`. Exec-form ENTRYPOINT cannot -# expand build args, so the binary lives at /usr/bin/${SERVICE} (where smelt's +# expand build args, so the binary lives at /usr/bin/${BIN} (where smelt's # WithServiceBinary mounts over it) with a fixed-name symlink for the # entrypoint to exec through. FROM --platform=$BUILDPLATFORM golang:1.27-bookworm AS build +ARG SERVICE ARG TARGETOS=linux ARG TARGETARCH WORKDIR /src -# Manifests first, for layer caching. -COPY go.mod go.sum* ./ +# Manifests first, for layer caching. Every module's go.mod/go.sum is copied, +# not just the service's, because go mod download follows the service's +# replace directives and needs the replaced modules' go.mod files in place. +COPY protocol/go.mod protocol/go.sum ./protocol/ +COPY internal/go.mod internal/go.sum ./internal/ +COPY attestation/go.mod attestation/go.sum ./attestation/ +COPY smelt/go.mod smelt/go.sum ./smelt/ +COPY indexing-service/go.mod indexing-service/go.sum ./indexing-service/ +COPY ${SERVICE}/go.mod ${SERVICE}/go.sum* ./${SERVICE}/ +WORKDIR /src/${SERVICE} RUN --mount=type=cache,target=/go/pkg/mod \ go mod download +WORKDIR /src COPY . . +# Every later stage builds from inside the service module. +WORKDIR /src/${SERVICE} # ---- production binary: stripped + static ---- FROM build AS build-prod @@ -76,18 +104,20 @@ RUN --mount=type=cache,target=/go/pkg/mod \ # ---- production runtime ---- FROM debian:bookworm-slim AS prod ARG SERVICE +ARG BIN=${SERVICE} RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ wget \ && rm -rf /var/lib/apt/lists/* -COPY --from=build-prod /out/app /usr/bin/${SERVICE} -RUN ln -s "/usr/bin/${SERVICE}" /usr/local/bin/service-entrypoint +COPY --from=build-prod /out/app /usr/bin/${BIN} +RUN ln -s "/usr/bin/${BIN}" /usr/local/bin/service-entrypoint ENTRYPOINT ["/usr/local/bin/service-entrypoint"] # ---- development runtime: prod + debug tooling + delve ---- FROM debian:bookworm-slim AS dev ARG SERVICE +ARG BIN=${SERVICE} RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ @@ -104,7 +134,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ tcpdump \ jq \ && rm -rf /var/lib/apt/lists/* -COPY --from=build-dev /out/app /usr/bin/${SERVICE} +COPY --from=build-dev /out/app /usr/bin/${BIN} COPY --from=build-dev /go/bin/dlv /usr/bin/dlv -RUN ln -s "/usr/bin/${SERVICE}" /usr/local/bin/service-entrypoint +RUN ln -s "/usr/bin/${BIN}" /usr/local/bin/service-entrypoint ENTRYPOINT ["/usr/local/bin/service-entrypoint"] diff --git a/go.work b/go.work index b2520a15..df23cb1d 100644 --- a/go.work +++ b/go.work @@ -1,9 +1,15 @@ go 1.27.0 use ( + ./attestation + ./delegator ./hilt + ./indexing-service ./ingot + ./internal ./piri + ./piri-signing-service + ./protocol ./smelt ./smelt/systems/stress-tester ./sprue diff --git a/hilt/AGENTS.md b/hilt/AGENTS.md index f2bb3c3b..08df9393 100644 --- a/hilt/AGENTS.md +++ b/hilt/AGENTS.md @@ -14,7 +14,7 @@ them. It exposes two APIs and talks to one external service: Module: `github.com/fil-forge/forge/hilt` (Go 1.27). Sibling repos it builds on: `ucantone` (UCAN primitives: `did`, `multikey`, `ucan/delegation`, `binding`, -`server`, `execution`), `libforge` (bound `commands/*`, `identity`, ucan helpers), +`server`, `execution`), the in-repo `protocol` (bound `commands/*`) and `internal` (`identity`, S3 helpers) modules, and `sprue` (the upload service; mirror its patterns where relevant). ## Commands @@ -54,18 +54,18 @@ and `sprue` (the upload service; mirror its patterns where relevant). - `pkg/api` — Tenant REST handlers + the partner-key auth middleware. - `pkg/rpc` — UCAN S3 command handlers; `pkg/rpc/service/auth` is the shared `Authorizer` service. The wire contract those handlers expose — named error - sentinels, `Operation`/`ClassifyRequest` — lives in libforge next to the + sentinels, `Operation`/`ClassifyRequest` — lives in `protocol/commands/s3` next to the `commands/s3` bindings (`commands/s3`, `commands/s3/request`, `commands/s3/bucket`), so Ingot consumes it without importing this module. -- SigV4 verification (`libforge/sigv4`) and the S3-permission → Forge-command - mapping (`libforge/s3perm`) also live in libforge for the same reason. +- SigV4 verification (`internal/sigv4`) and the S3-permission → Forge-command + mapping (`internal/s3perm`) also live in the in-repo `internal` module for the same reason. - `pkg/store/{tenant,accesskey,bucket,delegation,provider}` — each an interface with `memory` and `postgres` backends. - `pkg/vault` (`memory`, `openbao`) — private-key storage; `paths.go` has the key path helpers (`TenantKeyPath`, `AccessKeyPath`). - `pkg/client` — clients for external services (the Sprue `UploadClient`) and the self-issued `AdminClient`. The client for Hilt's own `/s3/*` RPC API is - `libforge/client/hilt` (Ingot and the integration tests use it). + `internal/client/hilt` (Ingot and the integration tests use it). - `pkg/migrations` — goose SQL migrations run on startup (unless skipped). - `internal/testutil` — test-only helpers (random DIDs/issuers, testcontainers). @@ -78,14 +78,14 @@ and `sprue` (the upload service; mirror its patterns where relevant). suite (`_test.go`). Add a method to all three (interface + both backends) and cover it in that suite. - **RPC handlers** follow one shape: a `NewHandler(logger, deps…) server.Route` - constructor that returns the libforge bound command's `.Route(...)`, whose closure + constructor that returns the `protocol` bound command's `.Route(...)`, whose closure extracts `req.Invocation().Issuer()` / `req.Task().Arguments()` and delegates to an **exported, testable** function (`ctx, logger, deps…, issuer, args`). That function returns `(*OK, []ucan.Delegation, error)`; the closure calls `res.SetFailure(err)` or `res.SetSuccess(ok)`, and attaches any delegation blocks via `res.SetMetadata(container.New(container.WithDelegations(blocks...)))` (the result's delegation map carries only CIDs — the blocks ride back in the container). -- **Use libforge bound commands** (`.Command`, `.Route`, `.Invoke`, `.Unpack`) — do +- **Use the `protocol` bound commands** (`.Command`, `.Route`, `.Invoke`, `.Unpack`) — do not hand-write command strings with `command.MustParse`. - **Authorization**: signature-bearing S3 commands authenticate via the `auth.Authorizer` service (SigV4/SigV4a verify + time bounds + issuer == tenant's diff --git a/hilt/Makefile b/hilt/Makefile index 81c47575..20db3879 100644 --- a/hilt/Makefile +++ b/hilt/Makefile @@ -15,7 +15,7 @@ export GOWORK := off # direct exec of a bare 'docker'. DOCKER := $(shell command -v docker) -# Container image via the monorepo-shared docker/Dockerfile (context = this dir). +# Container image via the monorepo-shared docker/Dockerfile (context = the repo root). IMAGE_BUILD_ARGS := --build-arg SERVICE=hilt .PHONY: build test itest vet image image-dev clean gen @@ -39,10 +39,10 @@ vet: go vet ./... image: - $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/hilt:local . + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/hilt:local .. image-dev: - $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/hilt:local-dev . + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/hilt:local-dev .. clean: rm $(BIN_DIR)/$(BINARY) diff --git a/hilt/go.mod b/hilt/go.mod index b10b306d..ea309f37 100644 --- a/hilt/go.mod +++ b/hilt/go.mod @@ -5,15 +5,16 @@ go 1.27.0 require ( github.com/alanshaw/dag-json-gen v0.0.9 github.com/aws/aws-sdk-go-v2 v1.42.0 - github.com/aws/aws-sdk-go-v2/config v1.32.24 - github.com/aws/aws-sdk-go-v2/credentials v1.19.23 - github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 - github.com/aws/smithy-go v1.27.2 + github.com/aws/aws-sdk-go-v2/config v1.32.26 + github.com/aws/aws-sdk-go-v2/credentials v1.19.25 + github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1 + github.com/aws/smithy-go v1.27.3 github.com/docker/docker v28.5.2+incompatible - github.com/fil-forge/libforge v0.0.0-20260904125112-81372e7200bf - github.com/fil-forge/smelt v0.0.0-20260828105933-8ba0939fb9a7 + github.com/fil-forge/forge/internal v0.0.0 + github.com/fil-forge/forge/protocol v0.0.0 + github.com/fil-forge/forge/smelt v0.0.0 github.com/fil-forge/swarf v0.0.1-0.20260821142121-d5d1a0a56f00 - github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 github.com/hashicorp/vault-client-go v0.4.3 github.com/ipfs/go-cid v0.6.2 github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 @@ -49,11 +50,11 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.30 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.2.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.31.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.43.4 // indirect github.com/buger/goterm v1.0.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -81,6 +82,7 @@ require ( github.com/ebitengine/purego v0.10.1 // indirect github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5 // indirect github.com/filecoin-project/go-data-segment v0.0.1 // indirect github.com/fsnotify/fsevents v0.2.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -117,7 +119,7 @@ require ( github.com/lib/pq v1.10.9 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/magiconair/properties v1.8.10 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect github.com/mattn/go-shellwords v1.0.13 // indirect @@ -222,3 +224,11 @@ require ( pitr.ca/jsontokenizer v0.3.2 // indirect tags.cncf.io/container-device-interface v1.1.0 // indirect ) + +replace github.com/fil-forge/forge/protocol => ../protocol + +replace github.com/fil-forge/forge/internal => ../internal + +replace github.com/fil-forge/forge/attestation => ../attestation + +replace github.com/fil-forge/forge/smelt => ../smelt diff --git a/hilt/go.sum b/hilt/go.sum index 765aaba6..70409e13 100644 --- a/hilt/go.sum +++ b/hilt/go.sum @@ -26,10 +26,10 @@ github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/by github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= -github.com/aws/aws-sdk-go-v2/config v1.32.24 h1:aEDEj533yGdVvEHfkCY0D/1FbDrjnZr4pIulxRjqpHs= -github.com/aws/aws-sdk-go-v2/config v1.32.24/go.mod h1:yZtrGKJGlqfEW+/m2uTsJK+Jz7xF5R0eZfgcIG9m1ss= -github.com/aws/aws-sdk-go-v2/credentials v1.19.23 h1:Zhu3GOpRCkNjtE/gJpuPDsytSnaCCTQk8neAGsgzG5Y= -github.com/aws/aws-sdk-go-v2/credentials v1.19.23/go.mod h1:VsJF2ropPB37gDr7M2rLSpCE8IQWdpl62uae7qxZmqU= +github.com/aws/aws-sdk-go-v2/config v1.32.26 h1:JI+W5B3jUA8UBz2ggbICGd9UCR6/+SB21G8EFl0SFTQ= +github.com/aws/aws-sdk-go-v2/config v1.32.26/go.mod h1:RLE2Ls/wRstvdSz1GPrIWNnXcKZ/znDdWyMuiQxdBoY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.25 h1:TzPVjfUZ1hsKafvYE+DIzKXIik2KufQxsPHanlkttbo= +github.com/aws/aws-sdk-go-v2/credentials v1.19.25/go.mod h1:K4hw0buguVvtC74HnVfTRr0LzQQHAWPqJbBU9QGk2Pg= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= @@ -44,20 +44,20 @@ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDs github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw= -github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.5 h1:6Xt6Ztjkwdia/7EtEaG7ki/qZUYlCcd7tGUotQed1QE= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.5/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= -github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk= -github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.30 h1:4HbXxyipSYxexU0juMIpdS05dilL6dbB2VQHxxN2vGU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.30/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1 h1:yb03KevaOAG5e8suo79Af74vjIQvoeKmjl79WQchLrs= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1/go.mod h1:mreYODw0Y4yv7xeczvqC6vciwFao8lPE9k1l1ulfY6E= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.1 h1:BeJmkm5YOZs6lGRGcNoIuLSoTTtGLLCEqlSiRKYodfM= +github.com/aws/aws-sdk-go-v2/service/signin v1.2.1/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.4 h1:i465b/3c7xJd++pobNIDOggouekCuiWOnB0goQJy+94= +github.com/aws/aws-sdk-go-v2/service/sso v1.31.4/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7 h1:xbmJAnBbyYPkTzoCNCF/bpJ6ymQHRdXX1vquYfDIGYk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.4 h1:Np0vmL7op0Zs5xGacYMMX3v5O5pvZ46xhb5LwDgPj8M= +github.com/aws/aws-sdk-go-v2/service/sts v1.43.4/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= +github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY= +github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= @@ -156,14 +156,12 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/fil-forge/libforge v0.0.0-20260904125112-81372e7200bf h1:FstLBgWd8aItdY2nG9fN4bleCMPIj3Q0u6/Sj9+rBwo= -github.com/fil-forge/libforge v0.0.0-20260904125112-81372e7200bf/go.mod h1:09nIz6/BJD0Qdb8YrLwVeNQwQprTCFWVPULL3c5CNJs= -github.com/fil-forge/smelt v0.0.0-20260828105933-8ba0939fb9a7 h1:0aFgIYRVCUGzs5CKrnxrjnEm1wxs+yT3f6ju3CBV5I0= -github.com/fil-forge/smelt v0.0.0-20260828105933-8ba0939fb9a7/go.mod h1:OUO2GxgUihmYfywNIfvWTQocg6jqaNiTd04cG5BbdII= +github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5 h1:Pu7Xk/LdaQyv1560k1MbL6CQUdWxZJIG9elEE0vkgLY= +github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5/go.mod h1:aL/KcXZ0KVs1ymAYqIwQyCnl2p481InN+gYbRLNM8fI= github.com/fil-forge/swarf v0.0.1-0.20260821142121-d5d1a0a56f00 h1:uSJ2YCxOJQA7PDnDiBJAOstoJcw2IPnbpssKYCihsxg= github.com/fil-forge/swarf v0.0.1-0.20260821142121-d5d1a0a56f00/go.mod h1:k1gay5Byjloml4k1SylxhRff1rMH4LcGKpbMEDLZR1o= -github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1 h1:Tzy3lZ7+LAyVK4+qyWM97UMOtnXx05Wdn+rcTDLdBDE= -github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= github.com/filecoin-project/go-data-segment v0.0.1 h1:1wmDxOG4ubWQm3ZC1XI5nCon5qgSq7Ra3Rb6Dbu10Gs= github.com/filecoin-project/go-data-segment v0.0.1/go.mod h1:H0/NKbsRxmRFBcLibmABv+yFNHdmtl5AyplYLnb0Zv4= github.com/filecoin-project/go-fil-commcid v0.3.1 h1:4EfxpHSlvtkOqa9weG2Yt5kxFmPib2xU7Uc9Lbqk7fs= @@ -315,8 +313,8 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= diff --git a/hilt/internal/testutil/alias.go b/hilt/internal/testutil/alias.go index 9edd4e38..34c85819 100644 --- a/hilt/internal/testutil/alias.go +++ b/hilt/internal/testutil/alias.go @@ -3,7 +3,7 @@ package testutil import ( "testing" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/ipfs/go-cid" ) diff --git a/hilt/itest/revocation_test.go b/hilt/itest/revocation_test.go index 777230c9..0789ce33 100644 --- a/hilt/itest/revocation_test.go +++ b/hilt/itest/revocation_test.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/smithy-go" - "github.com/fil-forge/forge/hilt/pkg/s3perm" + "github.com/fil-forge/forge/internal/s3perm" "github.com/stretchr/testify/require" ) diff --git a/hilt/itest/stack_test.go b/hilt/itest/stack_test.go index cdcfe042..e1132526 100644 --- a/hilt/itest/stack_test.go +++ b/hilt/itest/stack_test.go @@ -33,7 +33,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/fil-forge/forge/hilt/pkg/api" "github.com/fil-forge/forge/hilt/pkg/client/management" - "github.com/fil-forge/smelt/pkg/stack" + "github.com/fil-forge/forge/smelt/pkg/stack" swarfclient "github.com/fil-forge/swarf/pkg/client" swarfstore "github.com/fil-forge/swarf/pkg/store" "github.com/fil-forge/ucantone/did" diff --git a/hilt/pkg/api/service/accesskey/service.go b/hilt/pkg/api/service/accesskey/service.go index dde51090..8c80e8e0 100644 --- a/hilt/pkg/api/service/accesskey/service.go +++ b/hilt/pkg/api/service/accesskey/service.go @@ -13,13 +13,13 @@ import ( "slices" "time" - "github.com/fil-forge/forge/hilt/pkg/s3perm" "github.com/fil-forge/forge/hilt/pkg/store" accesskeystore "github.com/fil-forge/forge/hilt/pkg/store/accesskey" "github.com/fil-forge/forge/hilt/pkg/store/bucket" delegationstore "github.com/fil-forge/forge/hilt/pkg/store/delegation" "github.com/fil-forge/forge/hilt/pkg/store/tenant" "github.com/fil-forge/forge/hilt/pkg/vault" + "github.com/fil-forge/forge/internal/s3perm" swarfclient "github.com/fil-forge/swarf/pkg/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey" diff --git a/hilt/pkg/api/service/tenant/service_test.go b/hilt/pkg/api/service/tenant/service_test.go index 5a6f69f1..af5c0172 100644 --- a/hilt/pkg/api/service/tenant/service_test.go +++ b/hilt/pkg/api/service/tenant/service_test.go @@ -21,14 +21,14 @@ import ( wrapkeysmemory "github.com/fil-forge/forge/hilt/pkg/store/wrapkey/memory" "github.com/fil-forge/forge/hilt/pkg/vault" vaultmemory "github.com/fil-forge/forge/hilt/pkg/vault/memory" - customercmds "github.com/fil-forge/libforge/commands/customer" - ucanlib "github.com/fil-forge/libforge/ucan" + customercmds "github.com/fil-forge/forge/protocol/commands/customer" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/did/plc" "github.com/fil-forge/ucantone/multikey/secp256k1" "github.com/fil-forge/ucantone/server" "github.com/fil-forge/ucantone/ucan/container" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/stretchr/testify/require" "go.uber.org/zap" ) diff --git a/hilt/pkg/api/tenants_test.go b/hilt/pkg/api/tenants_test.go index 52138602..a3bd475a 100644 --- a/hilt/pkg/api/tenants_test.go +++ b/hilt/pkg/api/tenants_test.go @@ -29,8 +29,7 @@ import ( "github.com/fil-forge/forge/hilt/pkg/vault" vaultmemory "github.com/fil-forge/forge/hilt/pkg/vault/memory" "github.com/fil-forge/forge/hilt/pkg/wrapkey" - customercmds "github.com/fil-forge/libforge/commands/customer" - ucanlib "github.com/fil-forge/libforge/ucan" + customercmds "github.com/fil-forge/forge/protocol/commands/customer" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/did/plc" @@ -40,6 +39,7 @@ import ( "github.com/fil-forge/ucantone/ucan/command" "github.com/fil-forge/ucantone/ucan/container" "github.com/fil-forge/ucantone/ucan/delegation" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/labstack/echo/v4" "github.com/stretchr/testify/require" "go.uber.org/zap" diff --git a/hilt/pkg/client/admin.go b/hilt/pkg/client/admin.go index 4ab585ae..22c3be3f 100644 --- a/hilt/pkg/client/admin.go +++ b/hilt/pkg/client/admin.go @@ -6,7 +6,7 @@ import ( "net/url" adminprovider "github.com/fil-forge/forge/hilt/pkg/commands/admin/provider" - "github.com/fil-forge/forge/hilt/pkg/lib/zapucan" + "github.com/fil-forge/forge/internal/zapucan" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" diff --git a/hilt/pkg/client/upload/upload.go b/hilt/pkg/client/upload/upload.go index 1244c5e4..5357712f 100644 --- a/hilt/pkg/client/upload/upload.go +++ b/hilt/pkg/client/upload/upload.go @@ -6,17 +6,17 @@ import ( "net/http" "net/url" - "github.com/fil-forge/forge/hilt/pkg/lib/zapucan" - blobcmds "github.com/fil-forge/libforge/commands/blob" - customercmds "github.com/fil-forge/libforge/commands/customer" - providercmds "github.com/fil-forge/libforge/commands/provider" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/internal/zapucan" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + customercmds "github.com/fil-forge/forge/protocol/commands/customer" + providercmds "github.com/fil-forge/forge/protocol/commands/provider" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" "github.com/fil-forge/ucantone/ucan/invocation" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "go.uber.org/zap" ) diff --git a/hilt/pkg/client/upload/upload_test.go b/hilt/pkg/client/upload/upload_test.go index bbedad8e..1f0ed761 100644 --- a/hilt/pkg/client/upload/upload_test.go +++ b/hilt/pkg/client/upload/upload_test.go @@ -8,16 +8,16 @@ import ( "testing" upload "github.com/fil-forge/forge/hilt/pkg/client/upload" - blobcmds "github.com/fil-forge/libforge/commands/blob" - customercmds "github.com/fil-forge/libforge/commands/customer" - providercmds "github.com/fil-forge/libforge/commands/provider" - "github.com/fil-forge/libforge/testutil" - ucanlib "github.com/fil-forge/libforge/ucan" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + customercmds "github.com/fil-forge/forge/protocol/commands/customer" + providercmds "github.com/fil-forge/forge/protocol/commands/provider" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" ) diff --git a/hilt/pkg/commands/admin/provider/add.go b/hilt/pkg/commands/admin/provider/add.go index 6632f5b7..b605d89f 100644 --- a/hilt/pkg/commands/admin/provider/add.go +++ b/hilt/pkg/commands/admin/provider/add.go @@ -6,7 +6,7 @@ package provider import ( - "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/forge/protocol/commands" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/ucan/command" ) diff --git a/hilt/pkg/fx/identity.go b/hilt/pkg/fx/identity.go index 5375809b..aa75b73d 100644 --- a/hilt/pkg/fx/identity.go +++ b/hilt/pkg/fx/identity.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/fil-forge/forge/hilt/pkg/config" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "go.uber.org/fx" "go.uber.org/zap" ) diff --git a/hilt/pkg/fx/revocation_test.go b/hilt/pkg/fx/revocation_test.go index 12a6440d..949a939b 100644 --- a/hilt/pkg/fx/revocation_test.go +++ b/hilt/pkg/fx/revocation_test.go @@ -5,7 +5,7 @@ import ( "github.com/fil-forge/forge/hilt/pkg/config" appfx "github.com/fil-forge/forge/hilt/pkg/fx" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/stretchr/testify/require" ) diff --git a/hilt/pkg/fx/rpc.go b/hilt/pkg/fx/rpc.go index 292af558..0396e44f 100644 --- a/hilt/pkg/fx/rpc.go +++ b/hilt/pkg/fx/rpc.go @@ -8,7 +8,7 @@ import ( "github.com/fil-forge/forge/hilt/pkg/rpc" "github.com/fil-forge/forge/hilt/pkg/rpc/service/auth" bucketsvc "github.com/fil-forge/forge/hilt/pkg/rpc/service/bucket" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/did/key" "github.com/fil-forge/ucantone/did/resolver" "github.com/fil-forge/ucantone/did/web" diff --git a/hilt/pkg/fx/rpc_test.go b/hilt/pkg/fx/rpc_test.go index 2600bfa2..9ce607b5 100644 --- a/hilt/pkg/fx/rpc_test.go +++ b/hilt/pkg/fx/rpc_test.go @@ -14,8 +14,8 @@ import ( providermemory "github.com/fil-forge/forge/hilt/pkg/store/provider/memory" tenantmemory "github.com/fil-forge/forge/hilt/pkg/store/tenant/memory" vaultmemory "github.com/fil-forge/forge/hilt/pkg/vault/memory" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/testutil" "github.com/stretchr/testify/require" "go.uber.org/zap" ) diff --git a/hilt/pkg/fx/server.go b/hilt/pkg/fx/server.go index 353de088..f5960ecb 100644 --- a/hilt/pkg/fx/server.go +++ b/hilt/pkg/fx/server.go @@ -11,7 +11,7 @@ import ( "github.com/fil-forge/forge/hilt/pkg/build" "github.com/fil-forge/forge/hilt/pkg/config" "github.com/fil-forge/forge/hilt/pkg/echo/middleware" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/server" "github.com/labstack/echo/v4" echomiddleware "github.com/labstack/echo/v4/middleware" diff --git a/hilt/pkg/fx/upload.go b/hilt/pkg/fx/upload.go index 36d7234a..1f481ca2 100644 --- a/hilt/pkg/fx/upload.go +++ b/hilt/pkg/fx/upload.go @@ -7,10 +7,10 @@ import ( "github.com/fil-forge/forge/hilt/pkg/client/upload" "github.com/fil-forge/forge/hilt/pkg/config" - "github.com/fil-forge/libforge/identity" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan/container" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "go.uber.org/zap" ) diff --git a/hilt/pkg/fx/upload_test.go b/hilt/pkg/fx/upload_test.go index 82ff6f02..fc3c385a 100644 --- a/hilt/pkg/fx/upload_test.go +++ b/hilt/pkg/fx/upload_test.go @@ -7,7 +7,7 @@ import ( "github.com/fil-forge/forge/hilt/pkg/config" appfx "github.com/fil-forge/forge/hilt/pkg/fx" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/command" "github.com/fil-forge/ucantone/ucan/container" "github.com/fil-forge/ucantone/ucan/delegation" diff --git a/hilt/pkg/rpc/admin.go b/hilt/pkg/rpc/admin.go index 6a8b0d2c..c11c647f 100644 --- a/hilt/pkg/rpc/admin.go +++ b/hilt/pkg/rpc/admin.go @@ -8,7 +8,7 @@ import ( adminprovider "github.com/fil-forge/forge/hilt/pkg/commands/admin/provider" "github.com/fil-forge/forge/hilt/pkg/store" providerstore "github.com/fil-forge/forge/hilt/pkg/store/provider" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/did" ucanerrors "github.com/fil-forge/ucantone/errors" diff --git a/hilt/pkg/rpc/admin_test.go b/hilt/pkg/rpc/admin_test.go index 3098b79f..609976a1 100644 --- a/hilt/pkg/rpc/admin_test.go +++ b/hilt/pkg/rpc/admin_test.go @@ -7,7 +7,7 @@ import ( "github.com/fil-forge/forge/hilt/pkg/rpc" "github.com/fil-forge/forge/hilt/pkg/store" providermemory "github.com/fil-forge/forge/hilt/pkg/store/provider/memory" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/stretchr/testify/require" "go.uber.org/zap" ) diff --git a/hilt/pkg/rpc/authorize.go b/hilt/pkg/rpc/authorize.go index a988948d..ab70e17c 100644 --- a/hilt/pkg/rpc/authorize.go +++ b/hilt/pkg/rpc/authorize.go @@ -6,10 +6,10 @@ import ( "time" "github.com/fil-forge/forge/hilt/pkg/rpc/service/auth" - "github.com/fil-forge/forge/hilt/pkg/s3perm" - "github.com/fil-forge/forge/hilt/pkg/sigv4" - s3 "github.com/fil-forge/libforge/commands/s3" - s3req "github.com/fil-forge/libforge/commands/s3/request" + "github.com/fil-forge/forge/internal/s3perm" + "github.com/fil-forge/forge/internal/sigv4" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey" diff --git a/hilt/pkg/rpc/authorize_test.go b/hilt/pkg/rpc/authorize_test.go index 0596c2c0..0314f6b3 100644 --- a/hilt/pkg/rpc/authorize_test.go +++ b/hilt/pkg/rpc/authorize_test.go @@ -6,7 +6,6 @@ import ( "github.com/fil-forge/forge/hilt/pkg/rpc" "github.com/fil-forge/forge/hilt/pkg/rpc/service/auth" - "github.com/fil-forge/forge/hilt/pkg/sigv4" accesskeymemory "github.com/fil-forge/forge/hilt/pkg/store/accesskey/memory" bucketmemory "github.com/fil-forge/forge/hilt/pkg/store/bucket/memory" providermemory "github.com/fil-forge/forge/hilt/pkg/store/provider/memory" @@ -14,12 +13,13 @@ import ( tenantmemory "github.com/fil-forge/forge/hilt/pkg/store/tenant/memory" "github.com/fil-forge/forge/hilt/pkg/vault" vaultmemory "github.com/fil-forge/forge/hilt/pkg/vault/memory" - "github.com/fil-forge/libforge/commands/content" - s3 "github.com/fil-forge/libforge/commands/s3" - s3req "github.com/fil-forge/libforge/commands/s3/request" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/internal/sigv4" + "github.com/fil-forge/forge/protocol/commands/content" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey/ed25519" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/ipfs/go-cid" "github.com/multiformats/go-multibase" diff --git a/hilt/pkg/rpc/create.go b/hilt/pkg/rpc/create.go index b67836b5..8e22922e 100644 --- a/hilt/pkg/rpc/create.go +++ b/hilt/pkg/rpc/create.go @@ -2,8 +2,8 @@ package rpc import ( bucketsvc "github.com/fil-forge/forge/hilt/pkg/rpc/service/bucket" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" - s3req "github.com/fil-forge/libforge/commands/s3/request" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" "github.com/fil-forge/ucantone/ucan/container" diff --git a/hilt/pkg/rpc/delete.go b/hilt/pkg/rpc/delete.go index 1b5cd1df..b58bc888 100644 --- a/hilt/pkg/rpc/delete.go +++ b/hilt/pkg/rpc/delete.go @@ -2,7 +2,7 @@ package rpc import ( bucketsvc "github.com/fil-forge/forge/hilt/pkg/rpc/service/bucket" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" "go.uber.org/zap" diff --git a/hilt/pkg/rpc/failure.go b/hilt/pkg/rpc/failure.go index ddda7dac..b668cb4f 100644 --- a/hilt/pkg/rpc/failure.go +++ b/hilt/pkg/rpc/failure.go @@ -2,9 +2,8 @@ package rpc import ( "errors" - - "github.com/fil-forge/forge/hilt/pkg/rpc/service/auth" - bucketsvc "github.com/fil-forge/forge/hilt/pkg/rpc/service/bucket" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" ) // failer is the subset of *binding.Response[OK] used to record a receipt failure. @@ -17,19 +16,19 @@ type failer interface{ SetFailure(error) error } // unchanged; the dispatcher then reports them as a "HandlerExecutionError". func authFailure(res failer, err error) error { switch { - case errors.Is(err, auth.ErrMalformedSignature), - errors.Is(err, auth.ErrInvalidAccessKeyID), - errors.Is(err, auth.ErrUnknownAccessKey), - errors.Is(err, auth.ErrSignatureMismatch), - errors.Is(err, auth.ErrSignatureExpired), - errors.Is(err, auth.ErrAccessKeyExpired), - errors.Is(err, auth.ErrTenantDisabled), - errors.Is(err, auth.ErrIssuerForbidden), - errors.Is(err, auth.ErrRegionNotServed), - errors.Is(err, auth.ErrUnsupportedOperation), - errors.Is(err, auth.ErrOperationNotPermitted), - errors.Is(err, auth.ErrUnknownBucket), - errors.Is(err, auth.ErrBucketNotPermitted): + case errors.Is(err, s3req.ErrMalformedSignature), + errors.Is(err, s3req.ErrInvalidAccessKeyID), + errors.Is(err, s3req.ErrUnknownAccessKey), + errors.Is(err, s3req.ErrSignatureMismatch), + errors.Is(err, s3req.ErrSignatureExpired), + errors.Is(err, s3req.ErrAccessKeyExpired), + errors.Is(err, s3req.ErrTenantDisabled), + errors.Is(err, s3req.ErrIssuerForbidden), + errors.Is(err, s3req.ErrRegionNotServed), + errors.Is(err, s3req.ErrUnsupportedOperation), + errors.Is(err, s3req.ErrOperationNotPermitted), + errors.Is(err, s3req.ErrUnknownBucket), + errors.Is(err, s3req.ErrBucketNotPermitted): return res.SetFailure(err) default: return err @@ -53,13 +52,13 @@ func adminFailure(res failer, err error) error { // authFailure for the auth rejections the bucket service propagates from Authorize. func bucketFailure(res failer, err error) error { switch { - case errors.Is(err, bucketsvc.ErrOperationMismatch), - errors.Is(err, bucketsvc.ErrBucketExists), - errors.Is(err, bucketsvc.ErrBucketAlreadyOwned), - errors.Is(err, bucketsvc.ErrBucketNotEmpty), - errors.Is(err, bucketsvc.ErrUnknownBucket), - errors.Is(err, bucketsvc.ErrUnknownAccessKey), - errors.Is(err, bucketsvc.ErrInvalidArgument): + case errors.Is(err, s3bkt.ErrOperationMismatch), + errors.Is(err, s3bkt.ErrBucketExists), + errors.Is(err, s3bkt.ErrBucketAlreadyOwned), + errors.Is(err, s3bkt.ErrBucketNotEmpty), + errors.Is(err, s3bkt.ErrUnknownBucket), + errors.Is(err, s3bkt.ErrUnknownAccessKey), + errors.Is(err, s3bkt.ErrInvalidArgument): return res.SetFailure(err) default: return authFailure(res, err) diff --git a/hilt/pkg/rpc/failure_test.go b/hilt/pkg/rpc/failure_test.go index b7b18a61..7ee7d8c8 100644 --- a/hilt/pkg/rpc/failure_test.go +++ b/hilt/pkg/rpc/failure_test.go @@ -3,10 +3,10 @@ package rpc import ( "errors" "fmt" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "testing" - "github.com/fil-forge/forge/hilt/pkg/rpc/service/auth" - bucketsvc "github.com/fil-forge/forge/hilt/pkg/rpc/service/bucket" ucanerrors "github.com/fil-forge/ucantone/errors" "github.com/stretchr/testify/require" ) @@ -35,25 +35,25 @@ func requireName(t *testing.T, err error, want string) { func TestBucketFailure(t *testing.T) { t.Run("wrapped bucket sentinel is set as failure with its name", func(t *testing.T) { f := &recordingFailer{} - err := fmt.Errorf("%w: %q", bucketsvc.ErrBucketExists, "foo") + err := fmt.Errorf("%w: %q", s3bkt.ErrBucketExists, "foo") require.NoError(t, bucketFailure(f, err)) require.True(t, f.called) - requireName(t, f.got, bucketsvc.BucketExistsErrorName) + requireName(t, f.got, s3bkt.BucketExistsErrorName) }) t.Run("wrapped already-owned sentinel is set as failure with its name", func(t *testing.T) { f := &recordingFailer{} - err := fmt.Errorf("%w: %q", bucketsvc.ErrBucketAlreadyOwned, "foo") + err := fmt.Errorf("%w: %q", s3bkt.ErrBucketAlreadyOwned, "foo") require.NoError(t, bucketFailure(f, err)) require.True(t, f.called) - requireName(t, f.got, bucketsvc.BucketAlreadyOwnedErrorName) + requireName(t, f.got, s3bkt.BucketAlreadyOwnedErrorName) }) t.Run("propagated auth sentinel is set as failure with its name", func(t *testing.T) { f := &recordingFailer{} - require.NoError(t, bucketFailure(f, auth.ErrOperationNotPermitted)) + require.NoError(t, bucketFailure(f, s3req.ErrOperationNotPermitted)) require.True(t, f.called) - requireName(t, f.got, auth.OperationNotPermittedErrorName) + requireName(t, f.got, s3req.OperationNotPermittedErrorName) }) t.Run("unknown error is returned, not set as failure", func(t *testing.T) { @@ -92,15 +92,15 @@ func TestAdminFailure(t *testing.T) { func TestAuthFailure(t *testing.T) { t.Run("wrapped auth sentinel is set as failure with its name", func(t *testing.T) { f := &recordingFailer{} - err := fmt.Errorf("verifying signature: %w", auth.ErrSignatureMismatch) + err := fmt.Errorf("verifying signature: %w", s3req.ErrSignatureMismatch) require.NoError(t, authFailure(f, err)) require.True(t, f.called) - requireName(t, f.got, auth.SignatureMismatchErrorName) + requireName(t, f.got, s3req.SignatureMismatchErrorName) }) t.Run("bucket sentinel is unknown to authFailure and returned", func(t *testing.T) { f := &recordingFailer{} - require.ErrorIs(t, authFailure(f, bucketsvc.ErrBucketExists), bucketsvc.ErrBucketExists) + require.ErrorIs(t, authFailure(f, s3bkt.ErrBucketExists), s3bkt.ErrBucketExists) require.False(t, f.called) }) } diff --git a/hilt/pkg/rpc/info.go b/hilt/pkg/rpc/info.go index 34dde334..ef472d76 100644 --- a/hilt/pkg/rpc/info.go +++ b/hilt/pkg/rpc/info.go @@ -2,7 +2,7 @@ package rpc import ( bucketsvc "github.com/fil-forge/forge/hilt/pkg/rpc/service/bucket" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" "github.com/fil-forge/ucantone/ucan/container" diff --git a/hilt/pkg/rpc/list.go b/hilt/pkg/rpc/list.go index c3df87da..54c1be98 100644 --- a/hilt/pkg/rpc/list.go +++ b/hilt/pkg/rpc/list.go @@ -2,7 +2,7 @@ package rpc import ( bucketsvc "github.com/fil-forge/forge/hilt/pkg/rpc/service/bucket" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" "go.uber.org/zap" diff --git a/hilt/pkg/rpc/rpc_test.go b/hilt/pkg/rpc/rpc_test.go index 3e8543ad..a65dc590 100644 --- a/hilt/pkg/rpc/rpc_test.go +++ b/hilt/pkg/rpc/rpc_test.go @@ -14,9 +14,9 @@ import ( providermemory "github.com/fil-forge/forge/hilt/pkg/store/provider/memory" tenantmemory "github.com/fil-forge/forge/hilt/pkg/store/tenant/memory" vaultmemory "github.com/fil-forge/forge/hilt/pkg/vault/memory" - "github.com/fil-forge/libforge/identity" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/internal/identity" swarfclient "github.com/fil-forge/swarf/pkg/client" + "github.com/fil-forge/ucantone/testutil" "github.com/stretchr/testify/require" "go.uber.org/zap" ) diff --git a/hilt/pkg/rpc/service/auth/auth.go b/hilt/pkg/rpc/service/auth/auth.go index ab4ab06c..29c30350 100644 --- a/hilt/pkg/rpc/service/auth/auth.go +++ b/hilt/pkg/rpc/service/auth/auth.go @@ -8,17 +8,18 @@ import ( "context" "errors" "fmt" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "slices" "time" - "github.com/fil-forge/forge/hilt/pkg/sigv4" "github.com/fil-forge/forge/hilt/pkg/store" "github.com/fil-forge/forge/hilt/pkg/store/accesskey" "github.com/fil-forge/forge/hilt/pkg/store/bucket" "github.com/fil-forge/forge/hilt/pkg/store/provider" "github.com/fil-forge/forge/hilt/pkg/store/tenant" "github.com/fil-forge/forge/hilt/pkg/vault" - s3 "github.com/fil-forge/libforge/commands/s3" + "github.com/fil-forge/forge/internal/sigv4" + s3 "github.com/fil-forge/forge/protocol/commands/s3" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey" "github.com/fil-forge/ucantone/multikey/ed25519" @@ -39,7 +40,7 @@ type AuthorizedRequest struct { // Operation is the S3 operation the (signature-verified) request performs. The // access key is confirmed to hold its permission; handlers check it matches the // operation they serve. - Operation Operation + Operation s3.Operation // BucketName is the bucket name from the request. BucketName string // Bucket is the resolved bucket the request addresses, when the operation acts @@ -97,7 +98,7 @@ func (a *Authorizer) Authorize(ctx context.Context, issuer did.DID, req s3.Reque }) if err != nil { a.logger.Debug("rejecting unparseable request signature", zap.Error(err)) - return nil, ErrMalformedSignature + return nil, s3req.ErrMalformedSignature } log := a.logger.With(zap.String("access_key", sr.AccessKeyID), zap.Strings("regions", sr.Regions)) log.Debug("authorizing request") @@ -105,13 +106,13 @@ func (a *Authorizer) Authorize(ctx context.Context, issuer did.DID, req s3.Reque accessKeyID, err := did.Parse(did.KeyPrefix + sr.AccessKeyID) if err != nil { log.Debug("rejecting invalid access key id", zap.Error(err)) - return nil, ErrInvalidAccessKeyID + return nil, s3req.ErrInvalidAccessKeyID } akRec, err := a.accessKeys.Get(ctx, accessKeyID) if errors.Is(err, store.ErrRecordNotFound) { log.Debug("rejecting unknown access key") - return nil, ErrUnknownAccessKey + return nil, s3req.ErrUnknownAccessKey } else if err != nil { log.Error("looking up access key", zap.Error(err)) return nil, fmt.Errorf("looking up access key: %w", err) @@ -122,7 +123,7 @@ func (a *Authorizer) Authorize(ctx context.Context, issuer did.DID, req s3.Reque // (below) bounds the signature's freshness, not the credential's lifetime. if akRec.ExpiresAt != nil && time.Now().After(*akRec.ExpiresAt) { log.Debug("rejecting expired access key", zap.Timep("expires_at", akRec.ExpiresAt)) - return nil, ErrAccessKeyExpired + return nil, s3req.ErrAccessKeyExpired } // Authenticate: verify the request signature using the access key's secret. @@ -137,11 +138,11 @@ func (a *Authorizer) Authorize(ctx context.Context, issuer did.DID, req s3.Reque } if err := sigv4.Verify(sr, secret); err != nil { log.Debug("rejecting invalid request signature", zap.Error(err)) - return nil, ErrSignatureMismatch + return nil, s3req.ErrSignatureMismatch } if err := sigv4.ValidateTimeBounds(sr, time.Now()); err != nil { log.Debug("rejecting request outside its validity window", zap.Error(err)) - return nil, ErrSignatureExpired + return nil, s3req.ErrSignatureExpired } tenantRec, err := a.tenants.Get(ctx, akRec.Tenant) @@ -156,13 +157,13 @@ func (a *Authorizer) Authorize(ctx context.Context, issuer did.DID, req s3.Reque // handlers gate WriteLocked themselves, since Authorize is operation-agnostic. if tenantRec.Status == tenant.Disabled { log.Debug("rejecting disabled tenant") - return nil, ErrTenantDisabled + return nil, s3req.ErrTenantDisabled } // Only the tenant's provider may invoke on its behalf. if issuer != tenantRec.Provider { log.Debug("rejecting invocation not from the tenant's provider", zap.Stringer("issuer", issuer)) - return nil, ErrIssuerForbidden + return nil, s3req.ErrIssuerForbidden } // The request must be scoped to a region served by the tenant's provider. @@ -176,31 +177,31 @@ func (a *Authorizer) Authorize(ctx context.Context, issuer did.DID, req s3.Reque // Determine the S3 operation the (verified) request performs and confirm the // access key is permitted to perform it. The operation is returned so the // handler can check it matches the operation it serves. - op, bucketName, _, err := classifyRequest(req) + op, bucketName, _, err := s3.ClassifyRequest(req) if err != nil { log.Debug("rejecting unsupported operation", zap.Error(err)) - return nil, ErrUnsupportedOperation + return nil, s3req.ErrUnsupportedOperation } if !slices.Contains(akRec.Permissions, op.Permission()) { log.Debug("rejecting operation the access key lacks permission for", zap.Stringer("operation", op)) - return nil, ErrOperationNotPermitted + return nil, s3req.ErrOperationNotPermitted } // For operations on an existing bucket, resolve it (within the tenant) and // confirm it is within the access key's bucket scope (empty scope = all buckets). var resolved *bucket.Record - if op.addressesExistingBucket() { + if op.AddressesExistingBucket() { b, err := a.buckets.GetByName(ctx, bucketName) if errors.Is(err, store.ErrRecordNotFound) || (err == nil && b.Tenant != tenantRec.ID) { log.Debug("rejecting unknown bucket", zap.String("bucket", bucketName)) - return nil, ErrUnknownBucket + return nil, s3req.ErrUnknownBucket } else if err != nil { log.Error("looking up bucket", zap.Error(err)) return nil, fmt.Errorf("looking up bucket: %w", err) } if len(akRec.Buckets) > 0 && !slices.Contains(akRec.Buckets, b.ID) { log.Debug("rejecting bucket the access key is not scoped to", zap.String("bucket", bucketName)) - return nil, ErrBucketNotPermitted + return nil, s3req.ErrBucketNotPermitted } resolved = &b } @@ -270,5 +271,5 @@ func validateRegion(ctx context.Context, providers provider.Store, regions []str return r, nil } } - return "", ErrRegionNotServed + return "", s3req.ErrRegionNotServed } diff --git a/hilt/pkg/rpc/service/auth/auth_test.go b/hilt/pkg/rpc/service/auth/auth_test.go index b1f2241b..d936379d 100644 --- a/hilt/pkg/rpc/service/auth/auth_test.go +++ b/hilt/pkg/rpc/service/auth/auth_test.go @@ -1,11 +1,11 @@ package auth_test import ( + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "testing" "time" "github.com/fil-forge/forge/hilt/pkg/rpc/service/auth" - "github.com/fil-forge/forge/hilt/pkg/sigv4" accesskeymemory "github.com/fil-forge/forge/hilt/pkg/store/accesskey/memory" bucketmemory "github.com/fil-forge/forge/hilt/pkg/store/bucket/memory" providermemory "github.com/fil-forge/forge/hilt/pkg/store/provider/memory" @@ -13,12 +13,13 @@ import ( tenantmemory "github.com/fil-forge/forge/hilt/pkg/store/tenant/memory" "github.com/fil-forge/forge/hilt/pkg/vault" vaultmemory "github.com/fil-forge/forge/hilt/pkg/vault/memory" - s3 "github.com/fil-forge/libforge/commands/s3" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/internal/sigv4" + s3 "github.com/fil-forge/forge/protocol/commands/s3" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/multikey/secp256k1" + "github.com/fil-forge/ucantone/testutil" "github.com/multiformats/go-multibase" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -98,7 +99,7 @@ func TestAuthorize(t *testing.T) { require.Equal(t, accessKey.DID(), authz.AccessKey.ID) require.Equal(t, tenantID, authz.Tenant.ID) require.Equal(t, region, authz.Region) - require.Equal(t, auth.OpGetObject, authz.Operation) // GET /bucket/object-key + require.Equal(t, s3.OpGetObject, authz.Operation) // GET /bucket/object-key require.NotNil(t, authz.Bucket) require.Equal(t, "bucket", authz.Bucket.Name) require.NotNil(t, authz.Signed) @@ -108,14 +109,14 @@ func TestAuthorize(t *testing.T) { // The key is scoped to some other bucket, so it may not use "bucket". az, _, _ := setup(t, accessKey, &setupConfig{accessKeyBuckets: []did.DID{testutil.RandomDID(t)}}) _, err := az.Authorize(ctx, providerID, signedObjectRequest(t, accessKey, "bucket", region)) - require.ErrorIs(t, err, auth.ErrBucketNotPermitted) + require.ErrorIs(t, err, s3req.ErrBucketNotPermitted) }) t.Run("rejects an unknown bucket", func(t *testing.T) { // The key may use any bucket (nil scope), but "nope" does not exist. az, _, _ := setup(t, accessKey, nil) _, err := az.Authorize(ctx, providerID, signedObjectRequest(t, accessKey, "nope", region)) - require.ErrorIs(t, err, auth.ErrUnknownBucket) + require.ErrorIs(t, err, s3req.ErrUnknownBucket) }) t.Run("rejects an operation the access key lacks permission for", func(t *testing.T) { @@ -128,7 +129,7 @@ func TestAuthorize(t *testing.T) { signed, err := sigv4.Presign(req, accessKey.KeyDID().Identifier(), secret, region, sigv4.SchemeV4, time.Now(), time.Hour) require.NoError(t, err) _, err = az.Authorize(ctx, providerID, s3.Request{Method: signed.Method, URL: signed.URL}) - require.ErrorIs(t, err, auth.ErrOperationNotPermitted) + require.ErrorIs(t, err, s3req.ErrOperationNotPermitted) }) t.Run("rejects an invalid signature", func(t *testing.T) { @@ -146,19 +147,19 @@ func TestAuthorize(t *testing.T) { az := auth.NewAuthorizer(zap.NewNop(), accessKeys, tenants, providers, bucketmemory.New(), secrets) _, err = az.Authorize(ctx, providerID, signedRequest(t, accessKey, region, time.Now(), time.Hour)) - require.ErrorIs(t, err, auth.ErrSignatureMismatch) + require.ErrorIs(t, err, s3req.ErrSignatureMismatch) }) t.Run("rejects an unsigned request", func(t *testing.T) { az, _, _ := setup(t, accessKey, nil) _, err := az.Authorize(ctx, providerID, s3.Request{Method: "GET", URL: "https://s3.fil.one/bucket/object-key"}) - require.ErrorIs(t, err, auth.ErrMalformedSignature) + require.ErrorIs(t, err, s3req.ErrMalformedSignature) }) t.Run("rejects an unknown access key", func(t *testing.T) { az := auth.NewAuthorizer(zap.NewNop(), accesskeymemory.New(), tenantmemory.New(), providermemory.New(), bucketmemory.New(), vaultmemory.New()) _, err := az.Authorize(ctx, providerID, signedRequest(t, accessKey, region, time.Now(), time.Hour)) - require.ErrorIs(t, err, auth.ErrUnknownAccessKey) + require.ErrorIs(t, err, s3req.ErrUnknownAccessKey) }) t.Run("rejects when the access key secret is missing from the vault", func(t *testing.T) { @@ -181,27 +182,27 @@ func TestAuthorize(t *testing.T) { // A provider exists in eu-west-1, but it isn't the tenant's provider. require.NoError(t, providers.Add(ctx, testutil.RandomDID(t), "eu-west-1")) _, err := az.Authorize(ctx, providerID, signedRequest(t, accessKey, "eu-west-1", time.Now(), time.Hour)) - require.ErrorIs(t, err, auth.ErrRegionNotServed) + require.ErrorIs(t, err, s3req.ErrRegionNotServed) }) t.Run("rejects a region no provider serves", func(t *testing.T) { az, _, _ := setup(t, accessKey, nil) // No provider is registered for eu-west-1, so validateRegion skips it. _, err := az.Authorize(ctx, providerID, signedRequest(t, accessKey, "eu-west-1", time.Now(), time.Hour)) - require.ErrorIs(t, err, auth.ErrRegionNotServed) + require.ErrorIs(t, err, s3req.ErrRegionNotServed) }) t.Run("rejects an expired presigned URL", func(t *testing.T) { az, _, _ := setup(t, accessKey, nil) // Validly signed, but two hours ago with only a one-hour window. _, err := az.Authorize(ctx, providerID, signedRequest(t, accessKey, region, time.Now().Add(-2*time.Hour), time.Hour)) - require.ErrorIs(t, err, auth.ErrSignatureExpired) + require.ErrorIs(t, err, s3req.ErrSignatureExpired) }) t.Run("rejects an invocation not from the tenant's provider", func(t *testing.T) { az, _, _ := setup(t, accessKey, nil) _, err := az.Authorize(ctx, testutil.RandomDID(t), signedRequest(t, accessKey, region, time.Now(), time.Hour)) - require.ErrorIs(t, err, auth.ErrIssuerForbidden) + require.ErrorIs(t, err, s3req.ErrIssuerForbidden) }) t.Run("rejects an expired access key", func(t *testing.T) { @@ -210,7 +211,7 @@ func TestAuthorize(t *testing.T) { past := time.Now().Add(-time.Hour) az, _, _ := setup(t, accessKey, &setupConfig{accessKeyExpires: &past}) _, err := az.Authorize(ctx, providerID, signedRequest(t, accessKey, region, time.Now(), time.Hour)) - require.ErrorIs(t, err, auth.ErrAccessKeyExpired) + require.ErrorIs(t, err, s3req.ErrAccessKeyExpired) }) t.Run("rejects a disabled tenant", func(t *testing.T) { @@ -218,7 +219,7 @@ func TestAuthorize(t *testing.T) { // the tenant is disabled (so disabled status is the only variable). az, _, _ := setup(t, accessKey, &setupConfig{tenantStatus: tenant.Disabled}) _, err := az.Authorize(ctx, providerID, signedRequest(t, accessKey, region, time.Now(), time.Hour)) - require.ErrorIs(t, err, auth.ErrTenantDisabled) + require.ErrorIs(t, err, s3req.ErrTenantDisabled) }) } diff --git a/hilt/pkg/rpc/service/bucket/service.go b/hilt/pkg/rpc/service/bucket/service.go index 23c75069..ebf45ce8 100644 --- a/hilt/pkg/rpc/service/bucket/service.go +++ b/hilt/pkg/rpc/service/bucket/service.go @@ -16,14 +16,14 @@ import ( "github.com/fil-forge/forge/hilt/pkg/client/upload" "github.com/fil-forge/forge/hilt/pkg/rpc/service/auth" - "github.com/fil-forge/forge/hilt/pkg/sigv4" "github.com/fil-forge/forge/hilt/pkg/store" "github.com/fil-forge/forge/hilt/pkg/store/accesskey" bucketstore "github.com/fil-forge/forge/hilt/pkg/store/bucket" delegationstore "github.com/fil-forge/forge/hilt/pkg/store/delegation" - s3 "github.com/fil-forge/libforge/commands/s3" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" - s3req "github.com/fil-forge/libforge/commands/s3/request" + "github.com/fil-forge/forge/internal/sigv4" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" swarfclient "github.com/fil-forge/swarf/pkg/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey" @@ -103,8 +103,8 @@ func (s *Service) Create(ctx context.Context, issuer did.DID, args *s3bkt.Create } accessKeyID := authz.AccessKey.ID - if authz.Operation != auth.OpCreateBucket { - return nil, nil, fmt.Errorf("%w: %s", ErrOperationMismatch, authz.Operation) + if authz.Operation != s3.OpCreateBucket { + return nil, nil, fmt.Errorf("%w: %s", s3bkt.ErrOperationMismatch, authz.Operation) } rec, err := s.buckets.GetByName(ctx, authz.BucketName) @@ -112,9 +112,9 @@ func (s *Service) Create(ctx context.Context, issuer did.DID, args *s3bkt.Create // An existing name owned by the requesting tenant is // BucketAlreadyOwnedByYou; owned by anyone else is BucketAlreadyExists. if rec.Tenant == authz.Tenant.ID { - return nil, nil, fmt.Errorf("%w: %q", ErrBucketAlreadyOwned, authz.BucketName) + return nil, nil, fmt.Errorf("%w: %q", s3bkt.ErrBucketAlreadyOwned, authz.BucketName) } - return nil, nil, fmt.Errorf("%w: %q", ErrBucketExists, authz.BucketName) + return nil, nil, fmt.Errorf("%w: %q", s3bkt.ErrBucketExists, authz.BucketName) } else if !errors.Is(err, store.ErrRecordNotFound) { return nil, nil, fmt.Errorf("looking up bucket: %w", err) } @@ -259,8 +259,8 @@ func (s *Service) Delete(ctx context.Context, issuer did.DID, args *s3bkt.Delete return nil, err } - if authz.Operation != auth.OpDeleteBucket { - return nil, fmt.Errorf("%w: %s", ErrOperationMismatch, authz.Operation) + if authz.Operation != s3.OpDeleteBucket { + return nil, fmt.Errorf("%w: %s", s3bkt.ErrOperationMismatch, authz.Operation) } // Verify the bucket is empty, listing its blobs via Sprue as the tenant (the @@ -274,7 +274,7 @@ func (s *Service) Delete(ctx context.Context, issuer did.DID, args *s3bkt.Delete return nil, fmt.Errorf("checking bucket is empty: %w", err) } if !empty { - return nil, fmt.Errorf("%w: %q", ErrBucketNotEmpty, authz.BucketName) + return nil, fmt.Errorf("%w: %q", s3bkt.ErrBucketNotEmpty, authz.BucketName) } if err := s.revokeDelegations(ctx, authz.Tenant.ID, authz.Bucket.ID, account); err != nil { @@ -350,8 +350,8 @@ func (s *Service) List(ctx context.Context, issuer did.DID, args *s3bkt.ListArgu } // Bind the verified signature to this handler's operation: reject a // (validly-signed) request for any other S3 operation. - if authz.Operation != auth.OpListBuckets { - return nil, fmt.Errorf("%w: %s", ErrOperationMismatch, authz.Operation) + if authz.Operation != s3.OpListBuckets { + return nil, fmt.Errorf("%w: %s", s3bkt.ErrOperationMismatch, authz.Operation) } // The ListBuckets options ride as query parameters on the signed URL, which @@ -367,7 +367,7 @@ func (s *Service) List(ctx context.Context, issuer did.DID, args *s3bkt.ListArgu if v := query.Get("max-buckets"); v != "" { max, err = strconv.Atoi(v) if err != nil || max < 1 || max > maxListBuckets { - return nil, fmt.Errorf("%w: max-buckets: %q", ErrInvalidArgument, v) + return nil, fmt.Errorf("%w: max-buckets: %q", s3bkt.ErrInvalidArgument, v) } } @@ -412,14 +412,14 @@ func (s *Service) List(ctx context.Context, issuer did.DID, args *s3bkt.ListArgu func (s *Service) Info(ctx context.Context, args *s3bkt.InfoArguments) (*s3bkt.InfoOK, []ucan.Delegation, error) { b, err := s.buckets.GetByName(ctx, args.Name) if errors.Is(err, store.ErrRecordNotFound) { - return nil, nil, fmt.Errorf("%w: %q", ErrUnknownBucket, args.Name) + return nil, nil, fmt.Errorf("%w: %q", s3bkt.ErrUnknownBucket, args.Name) } else if err != nil { return nil, nil, fmt.Errorf("looking up bucket: %w", err) } akRec, err := s.accessKeys.Get(ctx, args.AccessKey) if errors.Is(err, store.ErrRecordNotFound) { - return nil, nil, fmt.Errorf("%w: %q", ErrUnknownAccessKey, args.AccessKey) + return nil, nil, fmt.Errorf("%w: %q", s3bkt.ErrUnknownAccessKey, args.AccessKey) } else if err != nil { return nil, nil, fmt.Errorf("looking up access key: %w", err) } diff --git a/hilt/pkg/rpc/service/bucket/service_test.go b/hilt/pkg/rpc/service/bucket/service_test.go index 16774989..09c02aa5 100644 --- a/hilt/pkg/rpc/service/bucket/service_test.go +++ b/hilt/pkg/rpc/service/bucket/service_test.go @@ -3,6 +3,7 @@ package bucket_test import ( "context" "errors" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "strings" "testing" "time" @@ -10,7 +11,6 @@ import ( "github.com/fil-forge/forge/hilt/pkg/client/upload" "github.com/fil-forge/forge/hilt/pkg/rpc/service/auth" bucketsvc "github.com/fil-forge/forge/hilt/pkg/rpc/service/bucket" - "github.com/fil-forge/forge/hilt/pkg/sigv4" "github.com/fil-forge/forge/hilt/pkg/store" accesskeymemory "github.com/fil-forge/forge/hilt/pkg/store/accesskey/memory" bucketmemory "github.com/fil-forge/forge/hilt/pkg/store/bucket/memory" @@ -21,15 +21,16 @@ import ( tenantmemory "github.com/fil-forge/forge/hilt/pkg/store/tenant/memory" "github.com/fil-forge/forge/hilt/pkg/vault" vaultmemory "github.com/fil-forge/forge/hilt/pkg/vault/memory" - "github.com/fil-forge/libforge/commands/content" - s3 "github.com/fil-forge/libforge/commands/s3" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/internal/sigv4" + "github.com/fil-forge/forge/protocol/commands/content" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" swarfclient "github.com/fil-forge/swarf/pkg/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/multikey/secp256k1" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/command" "github.com/fil-forge/ucantone/ucan/delegation" @@ -128,7 +129,7 @@ func TestCreate(t *testing.T) { t.Run("rejects a key without s3:CreateBucket", func(t *testing.T) { svc, _ := setup(t, []string{"s3:GetObject"}, &fakeSprue{}, delegationmemory.New()) _, _, err := svc.Create(ctx, providerID, args()) - require.ErrorIs(t, err, auth.ErrOperationNotPermitted) + require.ErrorIs(t, err, s3req.ErrOperationNotPermitted) }) t.Run("rejects a duplicate name owned by another tenant", func(t *testing.T) { @@ -136,7 +137,7 @@ func TestCreate(t *testing.T) { // Owner is a different tenant → BucketAlreadyExists. require.NoError(t, buckets.Add(ctx, testutil.RandomDID(t), testutil.RandomDID(t), bucketName)) _, _, err := svc.Create(ctx, providerID, args()) - require.ErrorIs(t, err, bucketsvc.ErrBucketExists) + require.ErrorIs(t, err, s3bkt.ErrBucketExists) }) t.Run("rejects re-creating a bucket you already own", func(t *testing.T) { @@ -144,7 +145,7 @@ func TestCreate(t *testing.T) { // Owner is the requesting tenant → BucketAlreadyOwnedByYou. require.NoError(t, buckets.Add(ctx, testutil.RandomDID(t), tenantID, bucketName)) _, _, err := svc.Create(ctx, providerID, args()) - require.ErrorIs(t, err, bucketsvc.ErrBucketAlreadyOwned) + require.ErrorIs(t, err, s3bkt.ErrBucketAlreadyOwned) }) t.Run("rolls back the bucket when provisioning fails", func(t *testing.T) { @@ -336,19 +337,19 @@ func TestDelete(t *testing.T) { t.Run("rejects a key without s3:DeleteBucket", func(t *testing.T) { d := setup(t, []string{"s3:GetObject"}, &fakeSprue{empty: true}) _, err := d.svc.Delete(ctx, providerID, del(bucketName)) - require.ErrorIs(t, err, auth.ErrOperationNotPermitted) + require.ErrorIs(t, err, s3req.ErrOperationNotPermitted) }) t.Run("rejects an unknown bucket", func(t *testing.T) { d := setup(t, []string{"s3:DeleteBucket"}, &fakeSprue{empty: true}) _, err := d.svc.Delete(ctx, providerID, del("nope")) - require.ErrorIs(t, err, auth.ErrUnknownBucket) + require.ErrorIs(t, err, s3req.ErrUnknownBucket) }) t.Run("rejects a non-empty bucket and keeps it", func(t *testing.T) { d := setup(t, []string{"s3:DeleteBucket"}, &fakeSprue{empty: false}) _, err := d.svc.Delete(ctx, providerID, del(bucketName)) - require.ErrorIs(t, err, bucketsvc.ErrBucketNotEmpty) + require.ErrorIs(t, err, s3bkt.ErrBucketNotEmpty) _, err = d.buckets.GetByName(ctx, bucketName) require.NoError(t, err) require.Empty(t, d.swarf.revocations, "nothing is revoked when the delete is refused") @@ -443,14 +444,14 @@ func TestList(t *testing.T) { svc, _, _ := setup(t, []string{"s3:ListAllMyBuckets"}) for _, param := range []string{"max-buckets=abc", "max-buckets=0", "max-buckets=-1", "max-buckets=10001"} { _, err := svc.List(ctx, providerID, listArgs(param)) - require.ErrorIs(t, err, bucketsvc.ErrInvalidArgument, "param %q", param) + require.ErrorIs(t, err, s3bkt.ErrInvalidArgument, "param %q", param) } }) t.Run("rejects a key without the list permission", func(t *testing.T) { svc, _, _ := setup(t, []string{"s3:GetObject"}) _, err := svc.List(ctx, providerID, listArgs()) - require.ErrorIs(t, err, auth.ErrOperationNotPermitted) + require.ErrorIs(t, err, s3req.ErrOperationNotPermitted) }) t.Run("rejects a validly-signed request for a different operation", func(t *testing.T) { @@ -460,7 +461,7 @@ func TestList(t *testing.T) { require.NoError(t, buckets.Add(ctx, testutil.RandomDID(t), tenantID, "bucket-a")) args := &s3bkt.ListArguments{Request: presign(t, signer, "GET", "https://"+region+".s3.fil.one/bucket-a/object-key", region)} _, err := svc.List(ctx, providerID, args) - require.ErrorIs(t, err, bucketsvc.ErrOperationMismatch) + require.ErrorIs(t, err, s3bkt.ErrOperationMismatch) }) } @@ -507,13 +508,13 @@ func TestInfo(t *testing.T) { t.Run("rejects an unknown bucket", func(t *testing.T) { svc := setup(t, did.DID{}) _, _, err := svc.Info(ctx, &s3bkt.InfoArguments{Name: "nope", AccessKey: akDID}) - require.ErrorIs(t, err, bucketsvc.ErrUnknownBucket) + require.ErrorIs(t, err, s3bkt.ErrUnknownBucket) }) t.Run("rejects an unknown access key", func(t *testing.T) { svc := setup(t, did.DID{}) _, _, err := svc.Info(ctx, &s3bkt.InfoArguments{Name: bucketName, AccessKey: testutil.RandomDID(t)}) - require.ErrorIs(t, err, bucketsvc.ErrUnknownAccessKey) + require.ErrorIs(t, err, s3bkt.ErrUnknownAccessKey) }) t.Run("returns empty delegations when no grant reaches the bucket", func(t *testing.T) { diff --git a/hilt/pkg/store/accesskey/accesskey_test.go b/hilt/pkg/store/accesskey/accesskey_test.go index 76423190..bdbe4ea1 100644 --- a/hilt/pkg/store/accesskey/accesskey_test.go +++ b/hilt/pkg/store/accesskey/accesskey_test.go @@ -14,8 +14,8 @@ import ( providerpostgres "github.com/fil-forge/forge/hilt/pkg/store/provider/postgres" "github.com/fil-forge/forge/hilt/pkg/store/tenant" tenantpostgres "github.com/fil-forge/forge/hilt/pkg/store/tenant/postgres" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/require" ) diff --git a/hilt/pkg/store/delegation/delegation_test.go b/hilt/pkg/store/delegation/delegation_test.go index 03ed2842..7b5ff374 100644 --- a/hilt/pkg/store/delegation/delegation_test.go +++ b/hilt/pkg/store/delegation/delegation_test.go @@ -10,8 +10,8 @@ import ( dlgstore "github.com/fil-forge/forge/hilt/pkg/store/delegation" delegationmemory "github.com/fil-forge/forge/hilt/pkg/store/delegation/memory" delegationpostgres "github.com/fil-forge/forge/hilt/pkg/store/delegation/postgres" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/command" "github.com/fil-forge/ucantone/ucan/delegation" diff --git a/hilt/pkg/store/delegation/memory/store.go b/hilt/pkg/store/delegation/memory/store.go index 49ee687f..52b6a425 100644 --- a/hilt/pkg/store/delegation/memory/store.go +++ b/hilt/pkg/store/delegation/memory/store.go @@ -10,9 +10,9 @@ import ( "github.com/fil-forge/forge/hilt/pkg/store" dlgstore "github.com/fil-forge/forge/hilt/pkg/store/delegation" - ucanlib "github.com/fil-forge/libforge/ucan" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" ) diff --git a/hilt/pkg/store/delegation/postgres/store.go b/hilt/pkg/store/delegation/postgres/store.go index c438bd0e..3921de6c 100644 --- a/hilt/pkg/store/delegation/postgres/store.go +++ b/hilt/pkg/store/delegation/postgres/store.go @@ -189,7 +189,7 @@ func (s *Store) DeleteBySubject(ctx context.Context, subject did.DID) error { // command to prove the child's command (the segment-boundary prefix test from // command.Command.Proves). A delegation whose subject equals its issuer is the // trust root and terminates a path. The shortest complete path is returned -// root-first, mirroring the in-memory store's use of libforge's ProofChain. +// root-first, mirroring the in-memory store's use of ucanlib's ProofChain. func (s *Store) ProofChain(ctx context.Context, aud did.DID, cmd ucan.Command, sub did.DID) ([]ucan.Delegation, []cid.Cid, error) { if !sub.Defined() { return nil, nil, fmt.Errorf("missing proof chain subject: %w", store.ErrInvalidArgument) diff --git a/hilt/pkg/store/provider/provider_test.go b/hilt/pkg/store/provider/provider_test.go index 2edbe62a..345578e9 100644 --- a/hilt/pkg/store/provider/provider_test.go +++ b/hilt/pkg/store/provider/provider_test.go @@ -9,8 +9,8 @@ import ( "github.com/fil-forge/forge/hilt/pkg/store/provider" providermemory "github.com/fil-forge/forge/hilt/pkg/store/provider/memory" providerpostgres "github.com/fil-forge/forge/hilt/pkg/store/provider/postgres" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" "github.com/stretchr/testify/require" ) diff --git a/hilt/pkg/store/tenant/tenant_test.go b/hilt/pkg/store/tenant/tenant_test.go index 93c8ab6a..5353f662 100644 --- a/hilt/pkg/store/tenant/tenant_test.go +++ b/hilt/pkg/store/tenant/tenant_test.go @@ -10,8 +10,8 @@ import ( "github.com/fil-forge/forge/hilt/pkg/store/tenant" tenantmemory "github.com/fil-forge/forge/hilt/pkg/store/tenant/memory" tenantpostgres "github.com/fil-forge/forge/hilt/pkg/store/tenant/postgres" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/require" ) diff --git a/indexing-service/.dockerignore b/indexing-service/.dockerignore new file mode 100644 index 00000000..93069294 --- /dev/null +++ b/indexing-service/.dockerignore @@ -0,0 +1,13 @@ +.git +.env* +*.md +LICENSE +LICENSE-* +docker-compose*.yml +.github +deploy +.tfworkspace +build +docs +.goreleaser.yaml +codecov.yml diff --git a/indexing-service/.gitignore b/indexing-service/.gitignore new file mode 100644 index 00000000..aeb115eb --- /dev/null +++ b/indexing-service/.gitignore @@ -0,0 +1,12 @@ +/.vscode + +/cmd/indexing-service +/indexing-service +*.car +/build +.terraform +.env +.tfworkspace +/ucangen +/indexer +.claude \ No newline at end of file diff --git a/indexing-service/.goreleaser.yaml b/indexing-service/.goreleaser.yaml new file mode 100644 index 00000000..4c5a3dfc --- /dev/null +++ b/indexing-service/.goreleaser.yaml @@ -0,0 +1,36 @@ +version: 2 + +builds: + - main: ./cmd + binary: indexing-service + ldflags: + # Sets the version variable in the build package to the build version prefixed with a 'v' + # Sets the main.date to a static date for checksum verification. See https://goreleaser.com/customization/builds/#reproducible-builds. + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser -X github.com/fil-forge/forge/indexing-service/pkg/build.version=v{{.Version}} + goos: + - linux + - windows + - darwin + goarch: + - 'amd64' + - 'arm64' + # Change to a static date for checksum verification. See https://goreleaser.com/customization/builds/#reproducible-builds. + mod_timestamp: '{{.CommitTimestamp}}' + env: + - CGO_ENABLED=0 +universal_binaries: + - replace: true +archives: + - format_overrides: + - goos: windows + format: zip + - goos: darwin + format: zip + name_template: >- + {{ .ProjectName }}_{{ .Version }}_ + {{- if eq .Os "darwin" }}mac_os + {{- else }}{{ .Os }}{{ end }}_{{ .Arch }} +release: + mode: keep-existing +changelog: + disable: true diff --git a/indexing-service/.mockery.yaml b/indexing-service/.mockery.yaml new file mode 100644 index 00000000..a8a92837 --- /dev/null +++ b/indexing-service/.mockery.yaml @@ -0,0 +1,62 @@ +dir: '{{.InterfaceDir}}' +structname: Mock{{.InterfaceName}} +template: testify +template-data: + unroll-variadic: true +filename: "mock_{{.InterfaceName}}.go" +packages: + github.com/aws/aws-sdk-go-v2/service/dynamodb: + interfaces: + QueryAPIClient: + config: + dir: pkg/internal/extmocks + structname: MockDynamoDBQueryClient + filename: mock_DynamoDBQueryClient.go + pkgname: extmocks + github.com/ipfs/go-log/v2: + interfaces: + EventLogger: + config: + dir: pkg/internal/extmocks + pkgname: extmocks + github.com/ipni/go-libipni/find/client: + interfaces: + Finder: + config: + dir: pkg/internal/extmocks + structname: MockIpniFinder + filename: mock_IpniFinder.go + pkgname: extmocks + github.com/fil-forge/forge/indexing-service/pkg/service/blobindexlookup: + interfaces: + BlobIndexLookup: {} + github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims: + config: + structname: MockContentClaims{{.InterfaceName}} + filename: "mock_ContentClaims{{.InterfaceName}}.go" + interfaces: + Finder: {} + Service: {} + github.com/fil-forge/forge/indexing-service/pkg/service/providercacher: + interfaces: + CachingQueue: {} + ProviderCacher: {} + github.com/fil-forge/forge/indexing-service/pkg/service/providerindex: + interfaces: + ProviderIndex: {} + github.com/fil-forge/forge/indexing-service/pkg/types: + interfaces: + ContentClaimsCache: {} + ContentClaimsStore: {} + NoProviderStore: {} + ProviderStore: {} + Service: {} + ValueSetCacheBatcher: {} + github.com/fil-forge/go-ipni-tools/pkg/publisher: + interfaces: + AsyncPublisher: + config: + dir: pkg/internal/extmocks + structname: MockIpniPublisher + pkgname: extmocks + filename: mock_IpniPublisher.go diff --git a/indexing-service/AGENTS.md b/indexing-service/AGENTS.md new file mode 100644 index 00000000..ab519dd3 --- /dev/null +++ b/indexing-service/AGENTS.md @@ -0,0 +1,159 @@ +# AGENTS.md — indexing-service + +Guidance for engineers and AI agents working in this repo. + +## Purpose + +Go service for content routing on the Forge network: a cache-and-query node that +bridges IPNI (InterPlanetary Network Indexer) with UCAN content claims. Clients and +gateways query it to find which storage nodes hold a given CID/multihash; storage and +upload services publish claims into it. Runs as a standalone HTTP server, deployed as +a container (ECS via the storoku Terraform module, port 8080). Lambda entry points +still exist under `cmd/lambda/` from the earlier Lambda/API-Gateway architecture, but +that is not how the service is deployed anymore. + +## Quick reference + +```bash +make indexer # go build -o ./indexer ./cmd +make test # go test -race -v ./... +make test-nocache # clear test cache, then test +make mockery # regenerate mocks (.mockery.yaml, testify template) +make gen # go generate ./... (cbor/json codegen, e.g. pkg/service/queryresult) +make ucankey # generate a multibase ed25519 private key for dev deployments + # (currently broken: depends on a `ucangen` binary with no build rule in-repo) + +go run ./cmd server # start standalone HTTP server +go run ./cmd aws # run as the containerized server used in AWS deployments (not a local Lambda emulator) +./indexer query # query an indexer node (default: https://indexer.forge.fil.one) +./indexer query -u # query a specific node +``` + +## Layout + +``` +cmd/ + main.go # CLI entry point: server | aws | query subcommands + server.go, aws.go, query.go + lambda/ # one main per Lambda: getroot, getclaim, getclaims, + # getdiddocument, postclaims, notifier, providercache, remotesync +pkg/ + server/server.go # HTTP mux, all routes, gzip + OTel wrapping + service/ + service.go # core query/publish/cache orchestration + contentclaims/ # claim stores/finders + UCAN server (ucanserver.go, ucanservice.go) + providerindex/ # IPNI provider index (+ remotesyncer/) + blobindexlookup/ # sharded DAG index lookup + providercacher/ # SQS-backed provider caching queue + queryresult/ # query result encoding (cbor_gen.go/json_gen.go are generated) + construct/construct.go # dependency injection: Construct(ServiceConfig, ...Option) + types/types.go # core interfaces: Store/Cache generics, Getter, Publisher, Querier, Service + aws/ # DynamoDB/S3/SQS/SNS adapters for the Lambda deployment + redis/ # Redis-backed stores (provider, claims, no-provider, sharded DAG index) + presets/ # preset endpoint/config values + telemetry/ # OpenTelemetry helpers + internal/jobwalker/ # parallel/single job walking used by query fan-out +deploy/ # OpenTofu/Terraform (app/ + shared/), invoked via deploy/Makefile +docs/infra.md # AWS infrastructure diagram (mermaid) — STALE: shows the old + # Lambda architecture; deploy/app/main.tf is the source of truth +``` + +## HTTP routes (pkg/server/server.go) + +| Route | Handler | Purpose | +|---|---|---| +| `GET /` | GetRootHandler | Service/version info | +| `GET /claim/{claim}` | GetClaimHandler | Fetch a single claim by root CID | +| `POST /` and `POST /claims` | PostClaimsHandler | UCAN invocations (both paths serve the same handler) | +| `GET /claims` | GetClaimsHandler (gzipped) | Query claims by multihash | +| `GET /.well-known/did.json` | GetDIDDocument | DID document | +| `GET /cid/{cid}` | GetIPNICIDHandler | IPNI lookup by CID (only registered when IPNI is configured) | + +## UCAN stack and capabilities + +- UCAN framework is **ucantone** (`github.com/fil-forge/ucantone`), not go-ucanto. + The UCAN server is `server.NewHTTP(id, ...)` with route registration via + `ucanServer.Handle(route.Command, route.Handler)` — see + `pkg/service/contentclaims/ucanserver.go`. +- Capability (command) definitions come from the in-repo **protocol** module + (`github.com/fil-forge/forge/protocol/commands/...`). Handlers bind typed + arguments/results via `binding.Request`/`binding.Response` and + `res.SetSuccess(...)` / `res.SetFailure(...)` — see `ucanservice.go`. + +Capabilities handled: + +- `assert/equals` (`protocol/commands/assert.Equals`) — publish an equals claim (originates from `filecoin/submit` handling) +- `assert/index` (`assert.Index`) — publish an index claim (originates from `index/add`) +- `claim/cache` (`protocol/commands/claim.Cache`) — cache a claim from a storage node, + keyed to the provider peer derived from the invocation issuer's did:key + +## Key integrations + +- **IPNI**: `github.com/ipni/go-libipni` (find/model, maurl) plus + `github.com/fil-forge/go-ipni-tools` (advertisement publishing, metadata). +- **AWS** (source of truth: `deploy/app/main.tf` + `deploy/app/legacyclaims.tf`): + DynamoDB tables `metadata` and `chunk-links`, plus read-only data sources for legacy + tables (claims, block index, allocations, store, blob registry); S3 buckets + `provider-caching`, `ipni-store` (public), `notifier-head`, `claim-store`, + `ipni-publisher`, plus the legacy claims bucket; SQS queues `provider-caching` + (standard), `ipni-publisher` (FIFO, high throughput), `ipni-advertisement-publishing` + (FIFO); Redis Serverless caches (providers, no-providers, indexes, claims); SSM + Parameter Store for the private key. No SNS topics or EventBridge schedulers are + provisioned (`topics = []`) — the diagram in `docs/infra.md` and parts of the README + describe the retired Lambda architecture and are out of sync with the Terraform; + trust the `.tf` files. +- **Telemetry**: OpenTelemetry (per-route `otelhttp` wrapping); Sentry for errors. + Honeycomb supported via `HONEYCOMB_API_KEY`; disable with `TELEMETRY_DISABLED`. + +## Conventions + +- Go 1.25.x module: `github.com/fil-forge/forge/indexing-service`. Files snake_case. +- Interfaces live next to implementations (`interface.go`); mocks are generated by + mockery into the same directory as `mock_.go` (testify template, + config in `.mockery.yaml`). Regenerate with `make mockery`. +- Tests: testify assertions, table/behavior style, `-race` always (via `make test`). +- Generated code: `cbor_gen.go` / `json_gen.go` files are produced by `make gen` + (go generate) — edit the generators/types, not the generated files. +- Service wiring happens in `pkg/construct/construct.go` (`Construct(ServiceConfig, ...Option)`); + add new dependencies there rather than constructing them ad hoc in handlers. + +## Deployment + +Terraform/OpenTofu under `deploy/` (storoku ECS app module), driven by +`deploy/Makefile` — run these from the `deploy/` directory, not the repo root +(the 25-line top-level Makefile only has build/test targets): + +- `make init` / `make validate` / `make plan` / `make apply` (also `upgrade`, + `console`, `clean`, `wait-deploy`) +- `make apply` builds a linux/arm64 image with `docker buildx`, pushes it to ECR, + and applies the Terraform with that image tag — there is no Lambda build step. +- Config via `deploy/.env.terraform` (copy from `deploy/.env.terraform.tpl`): + `TF_WORKSPACE` (your name; `prod`, `staging`, `warm-prod`, `warm-staging` are + reserved), `TF_VAR_app=indexer`, `TF_VAR_private_key` (`make ucankey`), + `TF_VAR_did`, `TF_VAR_allowed_account_id`, `TF_VAR_region`, optional + `TF_VAR_network`, `IPNI_ENDPOINT`, `IPNI_FALLBACK_ENDPOINTS`, + `IPNI_ANNOUNCE_URLS`, `IPNI_FORMAT_PEER_ID`/`IPNI_FORMAT_ENDPOINT`, + telemetry vars, and `TF_VAR_use_prod_vars`. (The README still says to copy + `.env.local` to `.env` — that file does not exist; the tpl is authoritative.) +- Runtime env for the container is rendered to `deploy/.env.production.local` + from its `.tpl` via `esh`. +- AWS credentials/region come from the standard AWS config chain — check your + profile before `make apply`. + +Release flow: merges to `main` auto-deploy staging. Production releases are cut by +bumping `version.json` on a branch; merging it triggers tagging, binary builds, a +GitHub release, and a production Terraform deploy (see `.github/workflows/`). + +## Blast radius / gotchas + +- Claim format or schema changes affect content routing for the entire network — + every client and gateway that queries this service. Capability schemas are defined + in `protocol`; changing them requires coordinating all publishers and consumers of + those commands across the network. +- IPNI advertisement changes affect content discoverability network-wide. +- `POST /` duplicating `POST /claims` is a deliberate temporary compatibility fix — + keep both wired until clients migrate. +- The `GET /cid/{cid}` route only exists when an IPNI config is provided; don't + assume it in tests or clients. +- Legacy stores (`legacy_claims`, `legacy_block_index` tables and the legacy claims + bucket) still serve reads — be careful when touching claim lookup paths. diff --git a/indexing-service/CLAUDE.md b/indexing-service/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/indexing-service/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/indexing-service/LICENSE-APACHE b/indexing-service/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/indexing-service/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/indexing-service/LICENSE-MIT b/indexing-service/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/indexing-service/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/indexing-service/Makefile b/indexing-service/Makefile new file mode 100644 index 00000000..f89b53c5 --- /dev/null +++ b/indexing-service/Makefile @@ -0,0 +1,49 @@ +indexer: + go build -o ./indexer ./cmd + +.PHONY: clean-indexer +clean-indexer: + rm -f ./indexer + +.PHONY: test +test: + go test -race -v ./... + +.PHONY: test-nocache +test-nocache: + go clean -testcache && make test + +.PHONY: ucankey +ucankey: ucangen + ./ucangen + +.PHONY: mockery +mockery: + mockery --config=.mockery.yaml + +gen: + go generate ./... + +# --- monorepo verb contract (see the root Makefile): vet, image, image-dev --- +# Resolve the docker binary to an absolute path: directories named 'docker' +# on PATH (including this repo's own docker/ when '.' is in PATH) break make's +# direct exec of a bare 'docker'. +DOCKER := $(shell command -v docker) + +# Container image via the monorepo-shared docker/Dockerfile (context = the repo root). +IMAGE_BUILD_ARGS := --build-arg SERVICE=indexing-service --build-arg BIN=indexer + +.PHONY: vet image image-dev +vet: + go vet ./... + +image: + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/indexing-service:local .. + +image-dev: + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/indexing-service:local-dev .. + +.PHONY: build clean +build: indexer + +clean: clean-indexer diff --git a/indexing-service/README.md b/indexing-service/README.md new file mode 100644 index 00000000..17f76dd7 --- /dev/null +++ b/indexing-service/README.md @@ -0,0 +1,169 @@ +# Indexing Services + +> Indexing for the Forge Network, Cached And Ready To Go + +## Table of Contents + +- [Overview](#overview) +- [Installation](#installation) +- [Deployment](#deployment) +- [CLI](#cli) +- [Contribute](#contribute) +- [License](#license) + +## Overview + +This is a cache and query node for finding content on Storacha quickly. + +## Installation + +Download the [indexing service binary from the latest release](https://github.com/fil-forge/forge/indexing-service/releases/latest) based on your system architecture, or download and install the [indexing-service](https://github.com/fil-forge/forge/indexing-service) package using the Go package manager: + +```bash +$ go install github.com/fil-forge/forge/indexing-service/cmd@latest + +... +``` + +## Deployment + +> [!TIP] +> Take a look at the [infrastructure diagram](docs/infra.md) for an overview of what gets deployed. + +Deployment of this service to AWS is managed by terraform which you can invoke with `make`. + +First, install OpenTofu e.g. + +```sh +brew install opentofu +``` + +or for Linux distributions that support Snap: + +```sh +snap install --classic opentofu +``` + +for other Operating Systems see: https://opentofu.org/docs/intro/install + +### AWS settings + +The terraform configuration will fetch AWS settings (such as credentials and the region to deploy resources to) from your local AWS configuration. Although an installation of the AWS CLI is not strictly required, it can be a convenient way to manage these settings. + +OpenTofu will go to the same places as the AWS CLI to find settings, which means it will read environment variables such as `AWS_REGION` and `AWS_PROFILE` and the `~/.aws/config` and `~/.aws/credentials` files. + +Make sure you are using the correct AWS profile and region before invoking `make` targets. + +### `.env` + +You need to first generate a .env with relevant vars. Copy `.env.local` to `.env` and then set the following environment variables: + +#### `TF_WORKSPACE` + +Best to set this to your name. "prod", "staging", "warm-prod", and "warm-staging" are reserved for shared deployments. + +#### `TF_VAR_private_key` + +This is a multibase encoded ed25519 private key used to sign receipts and for the indexer's peer ID. For development, you can generate one by running `make ucankey`. + +#### `TF_VAR_did` + +This is the DID for this deployment (did:web:... for example). e.g. + +```sh +TF_VAR_did='did:web:yourname.indexer.forge.fil.one' +``` + +#### `TF_VAR_principal_mapping` + +Optional. JSON encoded mapping of did:web to did:key. e.g. + +```sh +TF_VAR_principal_mapping={"did:web:example.com":"did:key:z6MktkCXwNmpqejQxYd7JHPcw7d4Srjct7sX74VLfKqsPyAw"} +``` + +#### `IPNI_ENDPOINT` + +Optional. HTTP endpoint of the IPNI instance used to discover providers. + +```sh +IPNI_ENDPOINT="https://cid.contact" +``` + +#### `IPNI_ANNOUNCE_URLS` + +Optional. JSON array of IPNI node URLs to announce chain updates to. + +```sh +IPNI_ANNOUNCE_URLS=["https://cid.contact/announce"] +``` + +#### `TELEMETRY_DISABLED` + +Telemetry is enabled by default. Set this variable to anything to disable it. + +When enabled, use the `HONEYCOMB_API_KEY` environment variable to have tracing data sent to Honeycomb. You can create a Honeycomb account and get an API key from the [Honeycomb website](https://www.honeycomb.io/). If a Honecomb API key is not provided, Storacha's default telemetry backend will be used. + +### Deployment commands + +Note that these commands will call needed prerequisites -- `make apply` will essentially do all of these start to finish. + +#### `make lambdas` + +This will simply compile the lambdas locally and put then in the `build` directory. + +#### `make init` + +You should only need to run this once -- initializes your terraform deployment and workspace. Make sure you've set `TF_WORKSPACE` first! + +If the `make init` fails you will need to execute `tofu init` directly from the `deploy/app` folder to install the required dependencies, and it will update the `.terraform.lock.hcl` file if needed. + +#### `make validate` + +This will validate your terraform configuration -- good to run to check errors in any changes you make to terraform configs. + +#### `make plan` + +This will plan a deployment, but not execute it -- useful to see ahead what changes will happen when you run the next deployment. + +#### `make apply` + +The big kahuna! This will deploy all of your changes, including redeploying lambdas if any of code changes. + +## CLI + +The command line interface can be used to query an indexer node. You'll need to compile the binary first: + +```sh +make indexer +``` + +#### `./indexer query ` + +Attempts to find the given CID in the Indexer node. The result can be multiple Location Claims, as there may be several storage nodes that store copies of the content, as well as potential indexes and the indexes themselves. If you want to query a specific node, you can use the following command: + +```sh +./indexer query -u https:// +``` + +If you don't specify a node it will query the Storacha Production node at https://indexer.forge.fil.one . + +## Releasing a new version + +Every time changes are merged to `main` the staging environment is automatically updated. Therefore, staging always runs the latest version of the code. The production environment, however, is only updated when a new version is released. + +The release process is automated in the repo's GitHub Actions workflows. Releasing a new version is as easy as updating the version in [version.json](version.json). + +When a branch with changes to [version.json](version.json) is merged to `main`, the release workflow will automatically tag the commit with the version, build and upload binaries, and create a new release. + +The release workflow not only publishes new releases. On successful runs, it will also trigger an additional deployment workflow that will deploy the new version to production by applying the Terraform configuration. + +## Contribute + +Early days PRs are welcome! + +## License + +This library is dual-licensed under Apache 2.0 and MIT terms. + +Copyright 2024. Storacha Network Inc. diff --git a/indexing-service/cmd/aws.go b/indexing-service/cmd/aws.go new file mode 100644 index 00000000..c04b58b2 --- /dev/null +++ b/indexing-service/cmd/aws.go @@ -0,0 +1,195 @@ +package main + +import ( + "fmt" + "time" + + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/fil-forge/forge/indexing-service/pkg/server" + "github.com/fil-forge/forge/indexing-service/pkg/service/providercacher" + "github.com/fil-forge/forge/indexing-service/pkg/service/providerindex/remotesyncer" + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + "github.com/fil-forge/go-ipni-tools/pkg/notifier" + "github.com/fil-forge/go-ipni-tools/pkg/publisher" + "github.com/fil-forge/go-ipni-tools/pkg/queue" + "github.com/fil-forge/go-ipni-tools/pkg/store" + "github.com/fil-forge/ucantone/did/key" + "github.com/fil-forge/ucantone/did/resolver" + "github.com/fil-forge/ucantone/did/web" + userver "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/validator" + goredis "github.com/redis/go-redis/v9" + "github.com/urfave/cli/v2" + "go.opentelemetry.io/otel/sdk/trace" +) + +var awsCmd = &cli.Command{ + Name: "aws", + Usage: "Run the indexing service as a containerized server in AWS", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: 8080, + Usage: "Port to bind the server to", + }, + }, + Action: func(cCtx *cli.Context) error { + addr := fmt.Sprintf(":%d", cCtx.Int("port")) + cfg := aws.FromEnv(cCtx.Context) + id := identity.Identity{Issuer: cfg.ID} + srvOpts := []server.Option{ + server.WithIdentity(id), + } + + wellKnownResolv, err := aws.NewPrincipalMappingResolver(cfg.PrincipalMapping) + if err != nil { + return fmt.Errorf("creating principal mapping resolver: %w", err) + } + + doc, err := id.DIDDocument() + if err != nil { + return fmt.Errorf("creating DID document: %w", err) + } + wellKnownResolv[cfg.ID.DID()] = doc + + httpResolv, err := web.NewResolver() + if err != nil { + return fmt.Errorf("creating HTTP resolver: %w", err) + } + + srvOpts = append( + srvOpts, + server.WithContentClaimsOptions( + userver.WithValidationOptions( + validator.WithDIDResolver(resolver.ByMethod{ + "key": key.Resolver, + "web": resolver.Tiered{ + wellKnownResolv, + resolver.NewCached(httpResolv, time.Hour*3), + }, + }), + ), + ), + ) + + ipniSrvOpts, err := ipniOpts(cfg.IPNIFormatPeerID, cfg.IPNIFormatEndpoint) + if err != nil { + return fmt.Errorf("setting up IPNI options: %w", err) + } + srvOpts = append(srvOpts, ipniSrvOpts...) + + if cfg.TelemetryEnabled { + var telemetryOpts []telemetry.TelemetryOption + if cfg.BaseTraceSampleRatio < 1.0 { + telemetryOpts = append(telemetryOpts, telemetry.WithBaseSampler(trace.TraceIDRatioBased(cfg.BaseTraceSampleRatio))) + } + telemetryShutdown, err := telemetry.SetupTelemetry(cCtx.Context, &cfg.Config, telemetryOpts...) + if err != nil { + panic(err) + } + defer telemetryShutdown(cCtx.Context) + + srvOpts = append(srvOpts, server.WithTelemetry()) + } + + indexer, err := aws.Construct(cfg) + if err != nil { + return err + } + + notifier, err := setupIPNIPipeline(cfg) + if err != nil { + return err + } + notifier.Start(cCtx.Context) + defer notifier.Stop() + + cacher, err := setupProviderCacher(cfg) + if err != nil { + return err + } + cacher.Start() + defer cacher.Stop() + + publisher, advertisementPublisher, err := setupIPNIPublisher(cfg) + if err != nil { + return err + } + publisher.Start() + defer publisher.Stop() + advertisementPublisher.Start() + defer advertisementPublisher.Stop() + + return server.ListenAndServe(addr, indexer, srvOpts...) + }, +} + +func setupIPNIPipeline(cfg aws.Config) (*notifier.Notifier, error) { + // setup remote IPNI syncer + providersRedis := goredis.NewClusterClient(&cfg.ProvidersRedis) + if cfg.TelemetryEnabled { + providersRedis = telemetry.InstrumentRedisClient(providersRedis) + } + providerStore := redis.NewProviderStore(redis.NewClusterClientAdapter(providersRedis)) + publisherStore := setupIPNIPublisherStore(cfg) + remoteSyncer := remotesyncer.New(providerStore, publisherStore) + + // setup notifier to periodically check IPNI and notify remote syncer if updates are required + headStore := aws.NewS3Store(cfg.Config, cfg.NotifierHeadBucket, "") + notifier, err := notifier.NewNotifierWithStorage(cfg.IPNIFindURL, cfg.PrivateKey, headStore) + if err != nil { + return nil, fmt.Errorf("creating notifier: %w", err) + } + + notifier.Notify(remoteSyncer.HandleRemoteSync) + return notifier, nil +} + +func setupProviderCacher(cfg aws.Config) (*providercacher.CachingQueuePoller, error) { + cachingQueue := aws.NewSQSCachingQueue(cfg.Config, cfg.SQSCachingQueueID, cfg.CachingBucket) + + providersRedis := goredis.NewClusterClient(&cfg.ProvidersRedis) + if cfg.TelemetryEnabled { + providersRedis = telemetry.InstrumentRedisClient(providersRedis) + } + providerStore := redis.NewProviderStore(redis.NewClusterClientAdapter(providersRedis)) + providerCacher := providercacher.NewSimpleProviderCacher(providerStore) + + return providercacher.NewCachingQueuePoller(cachingQueue, providerCacher) +} + +func setupIPNIPublisherStore(cfg aws.Config) *store.AdStore { + ipniStore := aws.NewS3Store(cfg.Config, cfg.IPNIStoreBucket, cfg.IPNIStorePrefix) + chunkLinksTable := aws.NewDynamoProviderContextTable(cfg.Config, cfg.ChunkLinksTableName) + metadataTable := aws.NewDynamoProviderContextTable(cfg.Config, cfg.MetadataTableName) + return store.NewPublisherStore(ipniStore, chunkLinksTable, metadataTable, store.WithMetadataContext(metadata.MetadataContext)) +} + +func setupIPNIPublisher(cfg aws.Config) (*queue.PublishingQueuePoller, *queue.AdvertisementPublishingQueuePoller, error) { + publishingQueue := aws.NewSQSPublishingQueue(cfg.Config, cfg.SQSPublishingQueueID, cfg.PublishingBucket) + advertisementPublishingQueue := aws.NewSQSAdvertisementPublishingQueue(cfg.Config, cfg.SQSAdvertisementPublishingQueueID) + store := setupIPNIPublisherStore(cfg) + advertisementQueuePublisher := queue.NewAdvertisementQueuePublisher(advertisementPublishingQueue, store) + publishingQueuePoller, err := queue.NewPublishingQueuePoller(publishingQueue, advertisementQueuePublisher) + if err != nil { + return nil, nil, fmt.Errorf("creating publishing queue poller: %w", err) + } + advertisementPublisher, err := publisher.NewAdvertisementPublisher( + cfg.ServiceConfig.PrivateKey, + store, + publisher.WithDirectAnnounce(cfg.ServiceConfig.IPNIDirectAnnounceURLs...), + publisher.WithAnnounceAddrs(cfg.ServiceConfig.IPNIAnnounceAddrs...), + ) + if err != nil { + return nil, nil, fmt.Errorf("creating IPNI advertisement publisher: %w", err) + } + advertisementPublishingQueuePoller, err := queue.NewAdvertisementPublishingQueuePoller(advertisementPublishingQueue, advertisementPublisher) + if err != nil { + return nil, nil, fmt.Errorf("creating advertisement publishing queue poller: %w", err) + } + return publishingQueuePoller, advertisementPublishingQueuePoller, nil +} diff --git a/indexing-service/cmd/lambda/getclaim/main.go b/indexing-service/cmd/lambda/getclaim/main.go new file mode 100644 index 00000000..f353ce50 --- /dev/null +++ b/indexing-service/cmd/lambda/getclaim/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + "github.com/fil-forge/forge/indexing-service/cmd/lambda" + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/server" +) + +func main() { + lambda.Start(makeHandler) +} + +func makeHandler(cfg aws.Config) any { + service, err := aws.Construct(cfg) + if err != nil { + panic(err) + } + + handler := httpadapter.NewV2(server.GetClaimHandler(service)).ProxyWithContext + + return handler +} diff --git a/indexing-service/cmd/lambda/getclaims/main.go b/indexing-service/cmd/lambda/getclaims/main.go new file mode 100644 index 00000000..e9d5e8e9 --- /dev/null +++ b/indexing-service/cmd/lambda/getclaims/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + "github.com/fil-forge/forge/indexing-service/cmd/lambda" + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/server" +) + +func main() { + lambda.Start(makeHandler) +} + +func makeHandler(cfg aws.Config) any { + service, err := aws.Construct(cfg) + if err != nil { + panic(err) + } + + handler := httpadapter.NewV2(server.GetClaimsHandler(service)).ProxyWithContext + + return handler +} diff --git a/indexing-service/cmd/lambda/getdiddocument/main.go b/indexing-service/cmd/lambda/getdiddocument/main.go new file mode 100644 index 00000000..6e47016b --- /dev/null +++ b/indexing-service/cmd/lambda/getdiddocument/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + "github.com/fil-forge/forge/indexing-service/cmd/lambda" + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/server" +) + +func main() { + lambda.Start(makeHandler) +} + +func makeHandler(cfg aws.Config) any { + handler := httpadapter.NewV2(server.GetDIDDocument(cfg.Issuer)).ProxyWithContext + return handler +} diff --git a/indexing-service/cmd/lambda/getroot/main.go b/indexing-service/cmd/lambda/getroot/main.go new file mode 100644 index 00000000..7169fcda --- /dev/null +++ b/indexing-service/cmd/lambda/getroot/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + "github.com/fil-forge/forge/indexing-service/cmd/lambda" + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/server" +) + +func main() { + lambda.Start(makeHandler) +} + +func makeHandler(cfg aws.Config) any { + handler := httpadapter.NewV2(server.GetRootHandler(cfg.Issuer)).ProxyWithContext + + return handler +} diff --git a/indexing-service/cmd/lambda/notifier/main.go b/indexing-service/cmd/lambda/notifier/main.go new file mode 100644 index 00000000..11481497 --- /dev/null +++ b/indexing-service/cmd/lambda/notifier/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "time" + + "github.com/aws/aws-lambda-go/events" + "github.com/fil-forge/forge/indexing-service/cmd/lambda" + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/go-ipni-tools/pkg/notifier" + logging "github.com/ipfs/go-log/v2" +) + +var log = logging.Logger("lambda/notifier") + +func main() { + lambda.Start(makeHandler) +} + +func makeHandler(cfg aws.Config) any { + // setup IPNI + // TODO: switch to double hashed client for reader privacy? + headStore := aws.NewS3Store(cfg.Config, cfg.NotifierHeadBucket, "") + notifier, err := notifier.NewNotifierWithStorage(cfg.IPNIFindURL, cfg.PrivateKey, headStore) + if err != nil { + panic(err) + } + sqsRemoteSyncNotifier := aws.NewSNSRemoteSyncNotifier(cfg.Config, cfg.NotifierTopicArn) + notifier.Notify(sqsRemoteSyncNotifier.NotifyRemoteSync) + + return func(ctx context.Context, event events.EventBridgeEvent) { + synced, ts, err := notifier.Update(ctx) + if err != nil { + log.Errorf("error during notifier sync head check: %s", err.Error()) + return + } + if !synced { + log.Warnf("remote IPNI subscriber did not sync for %s", time.Since(ts)) + } + } +} diff --git a/indexing-service/cmd/lambda/postclaims/main.go b/indexing-service/cmd/lambda/postclaims/main.go new file mode 100644 index 00000000..0a3b6e71 --- /dev/null +++ b/indexing-service/cmd/lambda/postclaims/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "fmt" + "time" + + "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter" + "github.com/fil-forge/forge/indexing-service/cmd/lambda" + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/server" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/ucantone/did/key" + "github.com/fil-forge/ucantone/did/resolver" + "github.com/fil-forge/ucantone/did/web" + ucanserver "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/validator" +) + +func main() { + lambda.Start(makeHandler) +} + +func makeHandler(cfg aws.Config) any { + service, err := aws.Construct(cfg) + if err != nil { + panic(err) + } + + wellKnownResolv, err := aws.NewPrincipalMappingResolver(cfg.PrincipalMapping) + if err != nil { + return fmt.Errorf("creating principal mapping resolver: %w", err) + } + + id := identity.Identity{Issuer: cfg.ID} + doc, err := id.DIDDocument() + if err != nil { + return fmt.Errorf("creating DID document: %w", err) + } + wellKnownResolv[cfg.ID.DID()] = doc + + httpResolv, err := web.NewResolver() + if err != nil { + return fmt.Errorf("creating HTTP resolver: %w", err) + } + + handler := httpadapter.NewV2( + server.PostClaimsHandler( + cfg.Issuer, + service, + ucanserver.WithValidationOptions( + validator.WithDIDResolver(resolver.ByMethod{ + "key": key.Resolver, + "web": resolver.Tiered{ + wellKnownResolv, + resolver.NewCached(httpResolv, time.Hour*3), + }, + }), + ), + ), + ).ProxyWithContext + + return handler +} diff --git a/indexing-service/cmd/lambda/providercache/main.go b/indexing-service/cmd/lambda/providercache/main.go new file mode 100644 index 00000000..188688b7 --- /dev/null +++ b/indexing-service/cmd/lambda/providercache/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "errors" + "sync" + "time" + + "github.com/aws/aws-lambda-go/events" + "github.com/fil-forge/forge/indexing-service/cmd/lambda" + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/fil-forge/forge/indexing-service/pkg/service/providercacher" + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + logging "github.com/ipfs/go-log/v2" + goredis "github.com/redis/go-redis/v9" +) + +var log = logging.Logger("lambda/providercache") + +// gracePeriod is the amount of time we have to clean up before a lambda exits +var gracePeriod = time.Second * 3 + +func main() { + lambda.Start(makeHandler) +} + +func makeHandler(cfg aws.Config) any { + providersRedis := goredis.NewClusterClient(&cfg.ProvidersRedis) + if cfg.TelemetryEnabled { + providersRedis = telemetry.InstrumentRedisClient(providersRedis) + } + providerStore := redis.NewProviderStore(redis.NewClusterClientAdapter(providersRedis)) + providerCacher := providercacher.NewSimpleProviderCacher(providerStore) + sqsCachingDecoder := aws.NewSQSCachingDecoder(cfg.Config, cfg.CachingBucket) + + return func(ctx context.Context, sqsEvent events.SQSEvent) (events.SQSEventResponse, error) { + deadline, ok := ctx.Deadline() + if ok { + graceDeadline := deadline.Add(-gracePeriod) + // if graceful shutdown time is after now then we can apply new deadline + if graceDeadline.After(time.Now()) { + dctx, cancel := context.WithDeadline(ctx, graceDeadline) + defer cancel() + ctx = dctx + } + } + + type handlerResult struct { + id string + err error + } + + // process messages in parallel + results := make(chan handlerResult, len(sqsEvent.Records)) + var wg sync.WaitGroup + for _, msg := range sqsEvent.Records { + wg.Add(1) + go func(msg events.SQSMessage) { + defer wg.Done() + err := handleMessage(ctx, sqsCachingDecoder, providerCacher, msg) + results <- handlerResult{msg.MessageId, err} + }(msg) + } + wg.Wait() + // collect errors + close(results) + batchItemFailures := []events.SQSBatchItemFailure{} + var err error + for r := range results { + if r.err != nil { + err = errors.Join(err, r.err) + batchItemFailures = append(batchItemFailures, events.SQSBatchItemFailure{ItemIdentifier: r.id}) + } + } + if err != nil { + log.Errorf("handling messages: %s", err.Error()) + } + return events.SQSEventResponse{BatchItemFailures: batchItemFailures}, nil + } +} + +func handleMessage(ctx context.Context, sqsCachingDecoder *aws.SQSCachingDecoder, providerCacher providercacher.ProviderCacher, msg events.SQSMessage) error { + job, err := sqsCachingDecoder.DecodeMessage(ctx, msg.ReceiptHandle, msg.Body) + if err != nil { + return err + } + err = providerCacher.CacheProviderForIndexRecords(ctx, job.Job.Provider, job.Job.Index) + // Do not hold up the queue by re-attempting a cache job that times out. It is + // probably a big DAG and retrying is unlikely to subsequently succeed. + if errors.Is(err, context.DeadlineExceeded) { + log.Warnf("not retrying cache provider job: %s", err) + return nil + } + if err != nil { + return err + } + return nil +} diff --git a/indexing-service/cmd/lambda/remotesync/main.go b/indexing-service/cmd/lambda/remotesync/main.go new file mode 100644 index 00000000..b18b3d44 --- /dev/null +++ b/indexing-service/cmd/lambda/remotesync/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-lambda-go/events" + "github.com/fil-forge/forge/indexing-service/cmd/lambda" + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/fil-forge/forge/indexing-service/pkg/service/providerindex/remotesyncer" + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + "github.com/fil-forge/go-ipni-tools/pkg/store" + "github.com/ipfs/go-cid" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + goredis "github.com/redis/go-redis/v9" +) + +func main() { + lambda.Start(makeHandler) +} + +func makeHandler(cfg aws.Config) any { + providersRedis := goredis.NewClusterClient(&cfg.ProvidersRedis) + if cfg.TelemetryEnabled { + providersRedis = telemetry.InstrumentRedisClient(providersRedis) + } + providerStore := redis.NewProviderStore(redis.NewClusterClientAdapter(providersRedis)) + ipniStore := aws.NewS3Store(cfg.Config, cfg.IPNIStoreBucket, cfg.IPNIStorePrefix) + chunkLinksTable := aws.NewDynamoProviderContextTable(cfg.Config, cfg.ChunkLinksTableName) + metadataTable := aws.NewDynamoProviderContextTable(cfg.Config, cfg.MetadataTableName) + publisherStore := store.NewPublisherStore(ipniStore, chunkLinksTable, metadataTable, store.WithMetadataContext(metadata.MetadataContext)) + remoteSyncer := remotesyncer.New(providerStore, publisherStore) + + return func(ctx context.Context, snsEvent events.SNSEvent) error { + for _, record := range snsEvent.Records { + snsRecord := record.SNS + var snsRemoteSyncMessage aws.SNSRemoteSyncMessage + err := json.Unmarshal([]byte(snsRecord.Message), &snsRemoteSyncMessage) + if err != nil { + return err + } + headCid, err := cid.Parse(snsRemoteSyncMessage.Head) + if err != nil { + return err + } + head := cidlink.Link{Cid: headCid} + prevCid, err := cid.Parse(snsRemoteSyncMessage.Prev) + if err != nil { + return err + } + prev := cidlink.Link{Cid: prevCid} + remoteSyncer.HandleRemoteSync(ctx, head, prev) + } + return nil + } +} diff --git a/indexing-service/cmd/lambda/start.go b/indexing-service/cmd/lambda/start.go new file mode 100644 index 00000000..e44e191a --- /dev/null +++ b/indexing-service/cmd/lambda/start.go @@ -0,0 +1,44 @@ +package lambda + +import ( + "context" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + "go.opentelemetry.io/otel/sdk/trace" +) + +// handlerFactory is a factory function that returns a function suitable to use as a lambda handler. See +// https://docs.aws.amazon.com/lambda/latest/dg/golang-handler.html#golang-handler-signatures for information on the +// valid signatures a handler function can have to be used as a lambda handler. +type handlerFactory func(cfg aws.Config) any + +// Start starts the lambda with the handler obtained from the factory function. makeHandler is a factory function that +// returns a handler suitable to use as a lambda handler. +// The handler is instrumented with OpenTelemetry if a Honeycomb API key is provided. +func Start(makeHandler handlerFactory) { + ctx := context.Background() + cfg := aws.FromEnv(ctx) + + // an empty API key disables instrumentation + if cfg.TelemetryEnabled { + var telemetryOpts []telemetry.TelemetryOption + if cfg.BaseTraceSampleRatio < 1.0 { + telemetryOpts = append(telemetryOpts, telemetry.WithBaseSampler(trace.TraceIDRatioBased(cfg.BaseTraceSampleRatio))) + } + telemetryShutdown, err := telemetry.SetupTelemetry(ctx, &cfg.Config, telemetryOpts...) + if err != nil { + panic(err) + } + + handler := makeHandler(cfg) + instrumentedHandler := telemetry.InstrumentLambdaHandler(handler) + + lambda.StartWithOptions(instrumentedHandler, lambda.WithContext(ctx), lambda.WithEnableSIGTERM(func() { + telemetryShutdown(ctx) + })) + } else { + lambda.StartWithOptions(makeHandler(cfg), lambda.WithContext(ctx)) + } +} diff --git a/indexing-service/cmd/main.go b/indexing-service/cmd/main.go new file mode 100644 index 00000000..2d461904 --- /dev/null +++ b/indexing-service/cmd/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + + logging "github.com/ipfs/go-log/v2" + "github.com/urfave/cli/v2" +) + +var log = logging.Logger("cmd") + +func main() { + logging.SetLogLevel("*", "info") + + app := &cli.App{ + Name: "indexing-service", + Usage: "Manage running the indexing service.", + Commands: []*cli.Command{ + serverCmd, + awsCmd, + queryCmd, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/indexing-service/cmd/query.go b/indexing-service/cmd/query.go new file mode 100644 index 00000000..b2fd2037 --- /dev/null +++ b/indexing-service/cmd/query.go @@ -0,0 +1,264 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "net/url" + + "github.com/fil-forge/forge/protocol/blobindex" + assertcaps "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/digestutil" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multibase" + "github.com/multiformats/go-multihash" + "github.com/urfave/cli/v2" + + "github.com/fil-forge/forge/indexing-service/pkg/client" + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + "github.com/fil-forge/forge/indexing-service/pkg/types" +) + +var queryCmd = &cli.Command{ + Name: "query", + Usage: "query an indexing server and print out the results", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "url", + Aliases: []string{"u"}, + Value: "https://indexer.forge.fil.one", + Usage: "URL of the indexer to query.", + }, + &cli.StringFlag{ + Name: "space", + Aliases: []string{"s"}, + Usage: "DID of a space to filter results by.", + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "type of query to perform ['standard' | 'location' | 'index_or_location']", + Value: "standard", + }, + &cli.StringFlag{ + Name: "delegations", + Aliases: []string{"d"}, + Usage: "a UCAN container of delegations allowing the indexer to fetch content from the space", + }, + &cli.BoolFlag{ + Name: "enabled-telemetry", + Usage: "propagate tracing context on query requests", + EnvVars: []string{"INDEXER_CLIENT_TELEMETRY_ENABLED"}, + }, + }, + Action: func(cCtx *cli.Context) error { + serviceURL, err := url.Parse(cCtx.String("url")) + if err != nil { + return fmt.Errorf("parsing service URL: %w", err) + } + + serviceDID, err := did.Parse(fmt.Sprintf("did:web:%s", serviceURL.Hostname())) + if err != nil { + return fmt.Errorf("parsing service DID: %w", err) + } + + // if telemetry is enabled for this query, setup the provider + type otelCloser = func(context.Context) error + var otelClose otelCloser + if cCtx.Bool("telemetry") { + otelClose, err = telemetry.SetupClientTelemetry(cCtx.Context) + if err != nil { + return fmt.Errorf("setting up telemetry: %w", err) + } + } + + c, err := client.New(serviceDID, *serviceURL, client.WithTelemetryEnabled(cCtx.Bool("telemetry"))) + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var cids []cid.Cid + for _, arg := range cCtx.Args().Slice() { + cid, err := parseCID(arg) + if err != nil { + return fmt.Errorf("parsing CID/multihash: %w", err) + } + cids = append(cids, cid) + } + if len(cids) == 0 { + return fmt.Errorf("missing CID/multihash for query: %w", err) + } + + var digests []multihash.Multihash + for _, cid := range cids { + digests = append(digests, cid.Hash()) + } + + var spaces []did.DID + if cCtx.IsSet("space") { + space, err := did.Parse(cCtx.String("space")) + if err != nil { + return fmt.Errorf("parsing space DID: %w", err) + } + spaces = append(spaces, space) + } + + queryType := types.QueryTypeStandard + if cCtx.IsSet("type") { + queryType, err = types.ParseQueryType(cCtx.String("type")) + if err != nil { + return fmt.Errorf("error in query type: %w", err) + } + } + + var delegations []ucan.Delegation + if cCtx.IsSet("delegations") { + ct, err := container.Decode([]byte(cCtx.String("delegations"))) + if err != nil { + return fmt.Errorf("parsing UCAN container: %w", err) + } + delegations = ct.Delegations() + } + + qr, err := c.QueryClaims(cCtx.Context, types.Query{ + Type: queryType, + Hashes: digests, + Match: types.Match{Subject: spaces}, + Delegations: delegations, + }) + if err != nil { + return fmt.Errorf("querying service: %w", err) + } + + blockmap := map[cid.Cid][]byte{} + for _, b := range qr.Blocks() { + blockmap[b.Link] = b.Data + } + + fmt.Println("") + fmt.Println("Query:") + fmt.Printf(" Hashes (%d):\n", len(digests)) + for _, digest := range digests { + fmt.Printf(" %s\n", digestutil.Format(digest)) + } + if len(spaces) > 0 { + fmt.Printf(" Spaces (%d):\n", len(spaces)) + for _, space := range spaces { + fmt.Printf(" %s\n", space.String()) + } + } + fmt.Println("") + fmt.Println("Results:") + fmt.Printf(" Claims (%d):\n", len(qr.Claims())) + for _, root := range qr.Claims() { + claimBytes, ok := blockmap[root] + if !ok { + return fmt.Errorf("missing claim block: %w", err) + } + claim, err := invocation.Decode(claimBytes) + if err != nil { + return fmt.Errorf("decoding claim: %w", err) + } + + fmt.Printf(" %s\n", claim.Link()) + fmt.Println(" Type:") + fmt.Printf(" %s\n", claim.Command()) + switch claim.Command() { + case assertcaps.Location.Command: + var args assertcaps.LocationArguments + if err := args.UnmarshalCBOR(bytes.NewReader(claim.ArgumentsBytes())); err != nil { + return fmt.Errorf("decoding %s arguments: %w", assertcaps.Location, err) + } + fmt.Println(" Content:") + fmt.Printf(" %s\n", digestutil.Format(args.Content)) + if args.Space != did.Undef { + fmt.Println(" Space:") + fmt.Printf(" %s\n", args.Space) + } + fmt.Println(" Locations:") + for _, location := range args.Location { + fmt.Printf(" %s\n", location.URL()) + } + if args.Range != nil { + fmt.Printf(" Range: %d-", args.Range.Start) + if args.Range.End != nil { + fmt.Printf("%d\n", *args.Range.End) + } else { + fmt.Println("") + } + } + case assertcaps.Equals.Command: + var args assertcaps.EqualsArguments + if err := args.UnmarshalCBOR(bytes.NewReader(claim.ArgumentsBytes())); err != nil { + return fmt.Errorf("decoding %s arguments: %w", assertcaps.Equals, err) + } + fmt.Println(" Content:") + fmt.Printf(" %s\n", digestutil.Format(args.Content)) + fmt.Println(" Equals:") + fmt.Printf(" %s\n", args.Equals) + case assertcaps.Index.Command: + var args assertcaps.IndexArguments + if err := args.UnmarshalCBOR(bytes.NewReader(claim.ArgumentsBytes())); err != nil { + return fmt.Errorf("decoding %s arguments: %w", assertcaps.Index.Command, err) + } + fmt.Println(" Index:") + fmt.Printf(" %s\n", args.Index) + default: + fmt.Println(" (Unknown Claim)") + } + } + + fmt.Println("") + fmt.Printf(" Indexes (%d):\n", len(qr.Indexes())) + for _, root := range qr.Indexes() { + indexBytes, ok := blockmap[root] + if !ok { + return fmt.Errorf("missing index block: %w", err) + } + index, err := blobindex.Extract(bytes.NewReader(indexBytes)) + if err != nil { + return fmt.Errorf("decoding index: %w", err) + } + + fmt.Printf(" %s\n", root) + fmt.Printf(" Shards (%d):\n", index.Shards().Size()) + for shard, slices := range index.Shards().Iterator() { + fmt.Printf(" %s\n", digestutil.Format(shard)) + fmt.Printf(" Slices (%d):\n", slices.Size()) + for digest, position := range slices.Iterator() { + fmt.Printf(" %s @ %d-%d\n", digestutil.Format(digest), position.Start, position.End) + } + } + } + + if otelClose != nil { + if err := otelClose(cCtx.Context); err != nil { + log.Warnf("failed to close telemetry provider: %s", err) + } + } + return nil + }, +} + +func parseCID(input string) (cid.Cid, error) { + c, err := cid.Parse(input) + if err == nil { + return c, nil + } + + _, b, err := multibase.Decode(input) + if err != nil { + return cid.Undef, err + } + + _, digest, err := multihash.MHFromBytes(b) + if err != nil { + return cid.Undef, err + } + + return cid.NewCidV1(cid.Raw, digest), nil +} diff --git a/indexing-service/cmd/server.go b/indexing-service/cmd/server.go new file mode 100644 index 00000000..016e3f4c --- /dev/null +++ b/indexing-service/cmd/server.go @@ -0,0 +1,348 @@ +package main + +import ( + crypto_ed25519 "crypto/ed25519" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "net/url" + "os" + "strings" + "time" + + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/did/key" + "github.com/fil-forge/ucantone/did/resolver" + "github.com/fil-forge/ucantone/did/web" + "github.com/fil-forge/ucantone/multikey" + "github.com/fil-forge/ucantone/multikey/ed25519" + userver "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/validator" + logging "github.com/ipfs/go-log/v2" + "github.com/ipni/go-libipni/maurl" + "github.com/ipni/go-libipni/metadata" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + goredis "github.com/redis/go-redis/v9" + "github.com/urfave/cli/v2" + + "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/construct" + "github.com/fil-forge/forge/indexing-service/pkg/presets" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/fil-forge/forge/indexing-service/pkg/server" +) + +var serverCmd = &cli.Command{ + Name: "server", + Usage: "HTTP server interface to the indexing service", + Subcommands: []*cli.Command{ + { + Name: "start", + Usage: "start an indexing service HTTP server", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: 9000, + Usage: "port to bind the server to", + }, + &cli.StringFlag{ + Name: "private-key", + Aliases: []string{"pk"}, + Usage: "base64 encoded private key identity for the server", + }, + &cli.StringFlag{ + Name: "key-file", + Aliases: []string{"kf"}, + Usage: "path to PEM-encoded Ed25519 private key file", + }, + &cli.StringFlag{ + Name: "did", + Usage: "DID of the server (only needs to be set if different from what is derived from the private key i.e. a did:web DID)", + }, + &cli.StringFlag{ + Name: "redis-url", + Aliases: []string{"redis"}, + EnvVars: []string{"REDIS_URL"}, + Usage: "url for a running redis database", + }, + &cli.StringFlag{ + Name: "redis-passwd", + Aliases: []string{"rp"}, + EnvVars: []string{"REDIS_PASSWD"}, + Usage: "passwd for redis", + }, + &cli.StringFlag{ + Name: "ipni-endpoint", + Aliases: []string{"ipni"}, + DefaultText: "Defaults to https://cid.contact", + Value: "https://cid.contact", + Usage: "HTTP endpoint of the IPNI instance used to discover providers.", + }, + &cli.StringFlag{ + Name: "ipni-fallback-endpoints", + Value: `["https://cid2.contact"]`, + Usage: "JSON array of IPNI node URLs to use as fallback endpoints.", + }, + &cli.StringFlag{ + Name: "ipni-announce-urls", + Value: `["https://cid.contact/announce"]`, + Usage: "JSON array of IPNI node URLs to announce chain updates to.", + }, + &cli.StringFlag{ + Name: "ipni-format-peer-id", + Usage: "Peer ID of the IPNI node to use for format announcements (enables endpoint mimicking IPNI).", + }, + &cli.StringFlag{ + Name: "ipni-format-endpoint", + Usage: "HTTP endpoint of the IPNI node to use for format announcements (enables endpoint mimicking IPNI).", + }, + &cli.StringSliceFlag{ + Name: "public-url", + EnvVars: []string{"PUBLIC_URL"}, + Usage: "Public URL(s) where the indexing service can be reached (for claim fetching). Can be specified multiple times or comma-separated in env var.", + }, + &cli.StringSliceFlag{ + Name: "resolve-did-web", + EnvVars: []string{"RESOLVE_DID_WEB"}, + Usage: "Patterns for restricting DID resolution via HTTP (fetches /.well-known/did.json). Can be specified multiple times or comma-separated in env var.", + }, + &cli.BoolFlag{ + Name: "insecure-did-resolution", + EnvVars: []string{"INSECURE_DID_RESOLUTION"}, + Usage: "Use HTTP instead of HTTPS for did:web resolution (for local development), used with --resolve-did-web", + }, + }, + Action: func(cCtx *cli.Context) error { + if cCtx.IsSet("private-key") && cCtx.IsSet("key-file") { + return fmt.Errorf("private-key and key-file are mutually exclusive") + } + + addr := fmt.Sprintf(":%d", cCtx.Int("port")) + var idSigner multikey.Signer + var err error + var opts []server.Option + + if cCtx.IsSet("key-file") { + // load from PEM file + idSigner, err = signerFromPEMFile(cCtx.String("key-file")) + if err != nil { + return fmt.Errorf("loading key from PEM file: %w", err) + } + } else if cCtx.IsSet("private-key") { + idSigner, err = ed25519.Parse(cCtx.String("private-key")) + if err != nil { + return fmt.Errorf("parsing server private key: %w", err) + } + } else { + // generate a new private key if one is not provided + idSigner, err = ed25519.Generate() + if err != nil { + return fmt.Errorf("generating server private key: %w", err) + } + } + + id := identity.Identity{ + Issuer: multikey.KeyIssuer(idSigner), + } + // use custom DID if specified + if cCtx.String("did") != "" { + customDID, err := did.Parse(cCtx.String("did")) + if err != nil { + return fmt.Errorf("parsing server DID: %w", err) + } + id = identity.Identity{ + Issuer: multikey.NewIssuer(customDID, idSigner), + } + } + + opts = append(opts, server.WithIdentity(id)) + + // Create DID resolver + resolveDIDPatterns := cCtx.StringSlice("resolve-did-web") + // Use HTTP-based resolution for specified patterns + for i, d := range resolveDIDPatterns { + // remove "did:web:" prefix if present, since the resolver expects + // just the domain/path part + resolveDIDPatterns[i] = strings.TrimPrefix(d, "did:web:") + } + rslvOpts := []web.Option{ + web.WithPatterns(resolveDIDPatterns...), + } + if cCtx.Bool("insecure-did-resolution") { + rslvOpts = append(rslvOpts, web.WithInsecure(true)) + } + + wellKnownResolv, err := aws.NewPrincipalMappingResolver(presets.PrincipalMapping) + if err != nil { + return fmt.Errorf("creating principal mapping resolver: %w", err) + } + + doc, err := id.DIDDocument() + if err != nil { + return fmt.Errorf("creating DID document: %w", err) + } + wellKnownResolv[id.DID()] = doc + + httpResolv, err := web.NewResolver(rslvOpts...) + if err != nil { + return fmt.Errorf("creating HTTP resolver: %w", err) + } + + opts = append( + opts, + server.WithContentClaimsOptions( + userver.WithValidationOptions( + validator.WithDIDResolver(resolver.ByMethod{ + "key": key.Resolver, + "web": resolver.Tiered{ + wellKnownResolv, + resolver.NewCached(httpResolv, time.Hour*3), + }, + }), + ), + ), + ) + + ipniSrvOpts, err := ipniOpts(cCtx.String("ipni-format-peer-id"), cCtx.String("ipni-format-endpoint")) + if err != nil { + return fmt.Errorf("setting up IPNI options: %w", err) + } + opts = append(opts, ipniSrvOpts...) + var sc construct.ServiceConfig + sc.ID = id + sc.IPNIFindURL = cCtx.String("ipni-endpoint") + sc.PublicURL = cCtx.StringSlice("public-url") + + // Create standalone Redis client for local development + redisOpts := &goredis.Options{ + Addr: cCtx.String("redis-url"), + Password: cCtx.String("redis-passwd"), + } + redisClient := goredis.NewClient(redisOpts) + clientAdapter := redis.NewClientAdapter(redisClient) + + if cCtx.String("ipni-fallback-endpoints") != "" { + var urls []string + err := json.Unmarshal([]byte(cCtx.String("ipni-fallback-endpoints")), &urls) + if err != nil { + return fmt.Errorf("parsing IPNI fallback endpoints JSON: %w", err) + } + sc.IPNIFindFallbackURLs = urls + } + + if cCtx.String("ipni-announce-urls") != "" { + var urls []string + err := json.Unmarshal([]byte(cCtx.String("ipni-announce-urls")), &urls) + if err != nil { + return fmt.Errorf("parsing IPNI announce URLs JSON: %w", err) + } + sc.IPNIDirectAnnounceURLs = urls + } else { + sc.IPNIDirectAnnounceURLs = presets.IPNIAnnounceURLs + } + + // id.Raw() returns the 32-byte seed; libp2p's + // UnmarshalEd25519PrivateKey wants the 64-byte stdlib form + // (seed||pub). Expand via NewKeyFromSeed before handing over. + privKey, err := crypto.UnmarshalEd25519PrivateKey(crypto_ed25519.NewKeyFromSeed(idSigner.Raw())) + if err != nil { + return fmt.Errorf("unmarshaling private key: %w", err) + } + sc.PrivateKey = privKey + + logging.SetAllLoggers(logging.LevelInfo) + indexer, err := construct.Construct(sc, + construct.WithProvidersClient(clientAdapter), + construct.WithNoProvidersClient(redisClient), + construct.WithClaimsClient(redisClient), + construct.WithIndexesClient(redisClient), + ) + if err != nil { + return err + } + err = indexer.Startup(cCtx.Context) + if err != nil { + return err + } + defer func() { + indexer.Shutdown(cCtx.Context) + }() + return server.ListenAndServe(addr, indexer, opts...) + }, + }, + }, +} + +func ipniOpts(ipniFormatPeerID string, ipniFormatEndpoint string) ([]server.Option, error) { + if ipniFormatEndpoint == "" || ipniFormatPeerID == "" { + return nil, nil + } + peerID, err := peer.Decode(ipniFormatPeerID) + if err != nil { + return nil, fmt.Errorf("decoding IPNI format peer ID: %w", err) + } + url, err := url.Parse(ipniFormatEndpoint) + if err != nil { + return nil, fmt.Errorf("parsing IPNI format endpoint URL: %w", err) + } + ma, err := maurl.FromURL(url) + if err != nil { + return nil, fmt.Errorf("converting IPNI format endpoint URL to multiaddr: %w", err) + } + return []server.Option{ + server.WithIPNI(peer.AddrInfo{ID: peerID, Addrs: []multiaddr.Multiaddr{ma}}, metadata.Default.New(metadata.IpfsGatewayHttp{})), + }, nil +} + +// signerFromPEMFile loads an Ed25519 private key from a PEM file. +func signerFromPEMFile(path string) (multikey.Signer, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + pemData, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("reading private key: %w", err) + } + + var privateKey *crypto_ed25519.PrivateKey + rest := pemData + + for { + block, remaining := pem.Decode(rest) + if block == nil { + break + } + rest = remaining + + if block.Type == "PRIVATE KEY" { + parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse PKCS#8 private key: %w", err) + } + + key, ok := parsedKey.(crypto_ed25519.PrivateKey) + if !ok { + return nil, fmt.Errorf("the parsed key is not an ED25519 private key") + } + privateKey = &key + break + } + } + + if privateKey == nil { + return nil, fmt.Errorf("could not find a PRIVATE KEY block in the PEM file") + } + + // ucantone's FromRaw wants the 32-byte seed; crypto/ed25519.PrivateKey is + // 64 bytes (seed || pub), so extract the seed before handing it over. + return ed25519.FromRaw(privateKey.Seed()) +} diff --git a/indexing-service/codecov.yml b/indexing-service/codecov.yml new file mode 100644 index 00000000..e239655f --- /dev/null +++ b/indexing-service/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "**/mock_*.go" \ No newline at end of file diff --git a/indexing-service/docs/infra.md b/indexing-service/docs/infra.md new file mode 100644 index 00000000..6e952f86 --- /dev/null +++ b/indexing-service/docs/infra.md @@ -0,0 +1,64 @@ +```mermaid +graph TB + %% VPC and Networking + VPC[VPC
10.0.0.0/16] --> PublicSubnets[Public Subnets] + VPC --> PrivateSubnets[Private Subnets] + PublicSubnets --> IGW[Internet Gateway] + PublicSubnets --> NAT[NAT Gateways] + NAT --> PrivateSubnets + + %% API Gateway + APIGW[API Gateway v2
HTTP API] --> Lambda + APIGW --> CustomDomain[Custom Domain
*.indexer.forge.fil.one] + CustomDomain --> Route53[Route53
DNS Zone] + CustomDomain --> ACM[ACM Certificate] + + %% Lambda Functions + subgraph Lambda[Lambda Functions] + GETroot + GETclaim + GETclaims + POSTclaims + notifier + providercache + remotesync + end + + %% Event Sources + EventBridge[EventBridge
Scheduler] --> notifier + SNSTopic[SNS Topic
Head Changes] --> remotesync + SQSQueue[SQS Queue
Caching.fifo] --> providercache + SQSQueue --> SQSDLQueue[Dead Letter Queue] + + %% Storage + Lambda --> DynamoDB + subgraph DynamoDB[DynamoDB Tables] + metadata + chunk_links + legacy_claims + legacy_block_index + end + + Lambda --> S3 + subgraph S3[S3 Buckets] + caching + ipni_store + notifier_head + claim_store + legacy_claims_bucket + end + + %% Cache + Lambda --> ElastiCache + subgraph ElastiCache[Redis Serverless] + providers + indexes + claims + end + + %% Parameters + Lambda --> SSM[SSM Parameter Store
Private Key] + + %% Security + SecurityGroup[Security Group
Lambda] --> CacheSecurityGroup[Security Group
Redis] +``` diff --git a/indexing-service/go.mod b/indexing-service/go.mod new file mode 100644 index 00000000..432fc85b --- /dev/null +++ b/indexing-service/go.mod @@ -0,0 +1,166 @@ +module github.com/fil-forge/forge/indexing-service + +go 1.27.0 + +require ( + github.com/alanshaw/dag-json-gen v0.0.9 + github.com/aws/aws-lambda-go v1.54.0 + github.com/aws/aws-sdk-go-v2 v1.45.1 + github.com/aws/aws-sdk-go-v2/config v1.33.2 + github.com/aws/aws-sdk-go-v2/credentials v1.20.2 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.21.2 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.66.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.110.0 + github.com/aws/aws-sdk-go-v2/service/sns v1.42.8 + github.com/aws/aws-sdk-go-v2/service/sqs v1.50.0 + github.com/aws/smithy-go v1.28.1 + github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 + github.com/benbjohnson/clock v1.3.5 + github.com/fil-forge/automobile v0.0.1 + github.com/fil-forge/forge/internal v0.0.0 + github.com/fil-forge/forge/protocol v0.0.0 + github.com/fil-forge/go-ipni-tools v0.0.0-20260820150418-78ca0c16834d + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 + github.com/getsentry/sentry-go v0.48.0 + github.com/google/uuid v1.6.0 + github.com/ipfs/go-cid v0.6.2 + github.com/ipfs/go-datastore v0.9.2 + github.com/ipfs/go-ds-flatfs v0.6.1 + github.com/ipfs/go-log/v2 v2.9.2 + github.com/ipld/go-ipld-prime v0.24.0 + github.com/ipni/go-libipni v0.8.2 + github.com/libp2p/go-libp2p v0.49.0 + github.com/multiformats/go-multiaddr v0.16.1 + github.com/multiformats/go-multibase v0.3.0 + github.com/multiformats/go-multicodec v0.10.0 + github.com/multiformats/go-multihash v0.2.3 + github.com/redis/go-redis/extra/redisotel/v9 v9.22.0 + github.com/redis/go-redis/v9 v9.22.0 + github.com/stretchr/testify v1.12.1 + github.com/testcontainers/testcontainers-go v0.44.0 + github.com/testcontainers/testcontainers-go/modules/minio v0.44.0 + github.com/testcontainers/testcontainers-go/modules/valkey v0.44.0 + github.com/urfave/cli/v2 v2.27.7 + github.com/whyrusleeping/cbor-gen v0.3.1 + go.opentelemetry.io/contrib/detectors/aws/ecs v1.46.0 + go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda v0.71.0 + go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.71.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 + go.opentelemetry.io/otel v1.46.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 + go.opentelemetry.io/otel/sdk v1.46.0 + go.opentelemetry.io/otel/trace v1.46.0 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.39.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.13.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.1 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.36.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.41.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.48.0 // indirect + github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20221221133751-67e37ae746cd // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v1.0.0-rc.4 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.10.1 // indirect + github.com/felixge/httpsnoop v1.1.0 // indirect + github.com/fil-forge/libforge v0.0.0-20260701162346-f0706e1641a3 // indirect + github.com/go-logr/logr v1.4.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/klauspost/compress v1.19.1 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/libp2p/go-libp2p-pubsub v0.17.0 // indirect + github.com/libp2p/go-msgio v0.3.0 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mdelapenya/tlscert v0.2.0 // indirect + github.com/minio/minio-go/v7 v7.0.94 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.55.0 // indirect + github.com/moby/moby/client v0.5.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.7.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect + github.com/multiformats/go-multistream v0.6.1 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect + github.com/onsi/gomega v1.37.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pion/webrtc/v4 v4.1.4 // indirect + github.com/polydawn/refmt v0.90.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.22.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shirou/gopsutil/v4 v4.26.6 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/tklauser/go-sysconf v0.4.0 // indirect + github.com/tklauser/numcpus v0.12.0 // indirect + github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect + go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/proto/otlp v1.11.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect + google.golang.org/grpc v1.83.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect + lukechampine.com/blake3 v1.4.1 // indirect + pitr.ca/jsontokenizer v0.3.2 // indirect +) + +replace github.com/fil-forge/forge/protocol => ../protocol + +replace github.com/fil-forge/forge/internal => ../internal diff --git a/indexing-service/go.sum b/indexing-service/go.sum new file mode 100644 index 00000000..ab1f5dd5 --- /dev/null +++ b/indexing-service/go.sum @@ -0,0 +1,541 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v1.0.0 h1:u0/Fhxlgz3uPv+XxhfgTq3BJt5VesIPM5ue/OuG7qjQ= +filippo.io/keygen v1.0.0/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/alanshaw/dag-json-gen v0.0.9 h1:q59Ra1mQB6HnFGI1ilFxsZvF1CqGN0XDlUWq16EJ6pw= +github.com/alanshaw/dag-json-gen v0.0.9/go.mod h1:v1YBZcS4B355MqxtyQr+fGNbEhm0CzHd+gOqOO/MZ+I= +github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 h1:iW0a5ljuFxkLGPNem5Ui+KBjFJzKg4Fv2fnxe4dvzpM= +github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5/go.mod h1:Y2QMoi1vgtOIfc+6DhrMOGkLoGzqSV2rKp4Sm+opsyA= +github.com/aws/aws-lambda-go v1.54.0 h1:EGYpdyRGF88xszqlGcBewz811mJeRS+maNlLZXFheII= +github.com/aws/aws-lambda-go v1.54.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= +github.com/aws/aws-sdk-go-v2 v1.45.1 h1:iIoG3NaLhV6UZpPXyPXlDj2I9oS8tV/nMcMnITCC6Ks= +github.com/aws/aws-sdk-go-v2 v1.45.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8= +github.com/aws/aws-sdk-go-v2/config v1.33.2 h1:Pj4+nF2kc4Z+1BJysVPnX9d5dMN7IYFXR4UJaWK2IpA= +github.com/aws/aws-sdk-go-v2/config v1.33.2/go.mod h1:Igw+HTwbR2tsTU/ydifAS9EHAFJ2s/FCgkwQWFnAdE4= +github.com/aws/aws-sdk-go-v2/credentials v1.20.2 h1:VQjZODPNfdikCX2ZZrltw4zNLkcwjyUFDUl2vT9yTwg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.2/go.mod h1:OmeHCn28vZylsBvalLDf7t8fuJ2rHYQprJs+7WuxniI= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.21.2 h1:KmZpAEo6YxXHcIxL1HT4iZo/JpctH8XQO2qwZoN62hU= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.21.2/go.mod h1:BKWPHURk3kEIUvddbvFqdjvIRwQuBxaGWrmgTwfKNTQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 h1:YIEBqcqRnpi4Pfv0YHImtgi6czGCwKHANC7SwmUAVD0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1/go.mod h1:imEf0oufgAo8KAkCHhrOdqGEC0YWx1PPBQH82shSxGw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 h1:pc138gM1CW+XPc60rEwUlwwuwWFQK16CI1T7v1F9Oec= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1/go.mod h1:1+koxpPIbfBdfzP6vojm5/zTpTQ/micYwlxIiNB3TxI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 h1:K0JsbZQj+1h208Ro1zHeA4l7bMp0NvRffHQ91q8Ol1s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1/go.mod h1:W3/vL6EtCIatICGy9ab29QhMuae+cOKPWcMxv02CO+Q= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 h1:yhw5KD1phVyP9vijxOUzDfEtJx+bt+L63k+VfuiYFAA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1/go.mod h1:ZW2e0d7DYlRxlS9hEiMXE47gTdX5KRN4byUiNbUpG+Q= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.66.0 h1:Iz90Uc1I8Yx/EVZMVEdB3SuLRkOJYY1YUUU0hTV7AFo= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.66.0/go.mod h1:1n7X9A0OjJ8CZjeac5PsFxEkIzBioOs223JPLDZ5bbQ= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.39.0 h1:A4GvY4y3TXcSoMFR6OPQ9SxDDulmUIzKT7865Jf1G3E= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.39.0/go.mod h1:5T5oAOfwOyP6UJJtHWoTWSHgOoUdrwgPAE8AHS6c9dI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.1 h1:s67hBfG5t9rn1NCvDuB4E3QIep3UFhHPtaIqFDjV3N8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.1/go.mod h1:FpvjBMXtSNMLPmDJsWwcY5cRnqJlpS2y1R6n4pvzs4k= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.13.1 h1:kIfDHRmq8zgw3DB0XxzGTZi19BX7OwIn+yDE9BJ2I1g= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.13.1/go.mod h1:o6YX/HIkSNfumuKUum+FrqrsebMFVJUiGcmGp03WnsQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 h1:RmmWQPREQdk9U+PfqeHW3MqZaBaNK7TpV9W3RY+b+7g= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1/go.mod h1:0A3W4F+68ZnNk5XcNL/e9HFMwnP8RlEicFfy6eOEDyw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.1 h1:ZMbtPZZQRca+3+XYQne9PBvRiYpHZlNJJOZfE9WNfT0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.1/go.mod h1:YAGWQdCYlVCoqrzvfv3RLxO6zKwti7gsAULOGWPLYv4= +github.com/aws/aws-sdk-go-v2/service/route53 v1.65.10 h1:8DaAa7LNudNOcUOjVGe9pEqYs1ASbryLS2bvrrPOXrA= +github.com/aws/aws-sdk-go-v2/service/route53 v1.65.10/go.mod h1:6amAo95XiktlgMb0blErtqRNw2+Lhz2pJsE1tNDQgUU= +github.com/aws/aws-sdk-go-v2/service/s3 v1.110.0 h1:He8vaTTqAAJrux/KdpjFXNWueLJZyKqE49QEXoqAu4I= +github.com/aws/aws-sdk-go-v2/service/s3 v1.110.0/go.mod h1:CUr46sCpGAg/rHaclRyhJX0LJAmH73uWSJPPSaMUrSk= +github.com/aws/aws-sdk-go-v2/service/signin v1.8.0 h1:bSvKIoLuRGFqGwASgeCQncCJDi9YKKBDEmCEZzOX1uU= +github.com/aws/aws-sdk-go-v2/service/signin v1.8.0/go.mod h1:9IqUlsJDbUPcg6cgx3WEzXdjrbWzLDQrak0aaSqlTcI= +github.com/aws/aws-sdk-go-v2/service/sns v1.42.8 h1:mD0Wp/ZWkyEhmZPJ3Egp2dZSNoxuWI3L0SIRtbm8rRM= +github.com/aws/aws-sdk-go-v2/service/sns v1.42.8/go.mod h1:R3ZSE4j64E01oumrJZ9kbTn5v6hqlmxSbfmcM1n1MrI= +github.com/aws/aws-sdk-go-v2/service/sqs v1.50.0 h1:iyOnIecB0y4rkOi4zeZO8iknl9h27cDCFW1tLP7HaKw= +github.com/aws/aws-sdk-go-v2/service/sqs v1.50.0/go.mod h1:d4DToDhLnEofHKvFu4yCF0Be65pZW267COfKOztsZOQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.36.0 h1:iivsh357VnfIc18IFWSuoyQEluf8frfWf4cL2Y0JUQw= +github.com/aws/aws-sdk-go-v2/service/sso v1.36.0/go.mod h1:tWuiVBUtPBr8/rgRiYS8Uf85sHcAN+G7XS3D3CEoUh8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.41.0 h1:wVxM3QzSKIK8tSN6OGgezp9OK91lCLH2zhmRInN9rFM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.41.0/go.mod h1:naFe83jSMuYkH+QjQPX8n1MLhBkeCFM5Lsnh5m5wz3c= +github.com/aws/aws-sdk-go-v2/service/sts v1.48.0 h1:RzZVCzYM19vhJCT5s6vO2wN8ie770Li/TmbAZ9B6N7E= +github.com/aws/aws-sdk-go-v2/service/sts v1.48.0/go.mod h1:mKo/CzaCz8qytGW70NG4vIIGAx1HXTlb5lHNkC5k3lk= +github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ= +github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/awslabs/aws-lambda-go-api-proxy v0.16.2 h1:CJyGEyO1CIwOnXTU40urf0mchf6t3voxpvUDikOU9LY= +github.com/awslabs/aws-lambda-go-api-proxy v0.16.2/go.mod h1:vxxjwBHe/KbgFeNlAP/Tvp4SsVRL3WQamcWRxqVh0z0= +github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= +github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20221221133751-67e37ae746cd h1:C0dfBzAdNMqxokqWUysk2KTJSMmqvh9cNW1opdy5+0Q= +github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20221221133751-67e37ae746cd/go.mod h1:CeKhh8xSs3WZAc50xABMxu+FlfAAd5PNumo7NfOv7EE= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= +github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= +github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= +github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= +github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/fil-forge/automobile v0.0.1 h1:9xB3yc4l5b9EdRJSJcNwudgBFNHoMPEAdcb7GfobLhA= +github.com/fil-forge/automobile v0.0.1/go.mod h1:TsO7jlO8ykJZY5tF8j4GsUcu3F02lEzxO7ULoB61hRA= +github.com/fil-forge/go-ipni-tools v0.0.0-20260820150418-78ca0c16834d h1:iEXJC5aJgRHq2iM4X6usfow436OQGauLjDuga1Hiq4o= +github.com/fil-forge/go-ipni-tools v0.0.0-20260820150418-78ca0c16834d/go.mod h1:fwLjqz34DvfoWcuzyH8Wr7dTJdr3mzYjxZRweysHOH4= +github.com/fil-forge/libforge v0.0.0-20260701162346-f0706e1641a3 h1:/EDxpbuVeSXH3FLF7MK4G4wrDUFmCO9fLQHL23RZ2uU= +github.com/fil-forge/libforge v0.0.0-20260701162346-f0706e1641a3/go.mod h1:0kXihIQ4L2uZ00nR5XrZ/Y8Db7Ht/qQNuiWslwMJ95M= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +github.com/filecoin-project/go-clock v0.1.0 h1:SFbYIM75M8NnFm1yMHhN9Ahy3W5bEZV9gd6MPfXbKVU= +github.com/filecoin-project/go-clock v0.1.0/go.mod h1:4uB/O4PvOjlx1VCMdZ9MyDZXRm//gkj1ELEbxfI1AZs= +github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= +github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/getsentry/sentry-go v0.48.0 h1:FRZNr7Uk1C86ev1bSJmYlUkL9oyivQA6YOcdYfaaMmY= +github.com/getsentry/sentry-go v0.48.0/go.mod h1:E5UkA5wp1qR2+MDydNYlVeUiNN2xEdjYMidkgf0Qoss= +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-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/ipfs/boxo v0.41.0 h1:diKlFosOG2e1mgSO1CXqcMSnHvtn6ubUvaCf9iF8AIY= +github.com/ipfs/boxo v0.41.0/go.mod h1:1Fo36UVVvq3XAZwMDD82Cm4JTUi5x1k3AsJlg9DttOY= +github.com/ipfs/go-block-format v0.2.4 h1:pgsT9i8zB4YQkBIQRrBwqbQiXPogRCiQnfxd2bC4koI= +github.com/ipfs/go-block-format v0.2.4/go.mod h1:YpXrOge8ARskfuJuqjvJTYr4v6o9IZWgK2EEIMAhpcU= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= +github.com/ipfs/go-datastore v0.9.2 h1:HJOgAmvWPRMHiwD8JHBzGZQNTKhuFGYfp8bNPwye28g= +github.com/ipfs/go-datastore v0.9.2/go.mod h1:VIjDxnINIcCqBMaB8LGggHfYY7PalKWfPtRMFeOU4q4= +github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= +github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= +github.com/ipfs/go-ds-flatfs v0.6.1 h1:oiC+iZPwlnsyNXHZuAZ/Ka2btw00s43pk80gIDWEcJM= +github.com/ipfs/go-ds-flatfs v0.6.1/go.mod h1:EddbXjWrzX39gCc6xn/HYGluTx3SmEy84KuWIVdApIs= +github.com/ipfs/go-log/v2 v2.9.2 h1:O/5BB0elpkRILvT24rCJ5976wWd7u0nJ436T3rdYdc4= +github.com/ipfs/go-log/v2 v2.9.2/go.mod h1:RziRwwXWhndlk8L75RnEe0zeAYaq2heKtEMc3jqUov0= +github.com/ipfs/go-test v0.4.1 h1:n6uNSakIgpTQIRorqNg2O02aMIFDLQk2z4rBfrlD3Uw= +github.com/ipfs/go-test v0.4.1/go.mod h1:QmvVBf9kClNtRuFow4DASq03eFvjKla4Fy/UAkeeLO8= +github.com/ipld/go-ipld-prime v0.24.0 h1:6th8Z6Peh5bCWuRAVZcDO1sHzZdVF6F2cCCDG3681tg= +github.com/ipld/go-ipld-prime v0.24.0/go.mod h1:DYZxr/5caLNFbcuU6zLOgwSW7CgUEoC4wJiZMEU8Zhs= +github.com/ipni/go-libipni v0.8.2 h1:mRJe602pD3WKcl/2WJFT6CjdI181/DcThqEdRL0usA8= +github.com/ipni/go-libipni v0.8.2/go.mod h1:YlujVRDn3uksrYeV62ERtqKQdRVgbnjF13wPsnCCHAk= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= +github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/koron/go-ssdp v0.9.1 h1:zvxbAAuJftJIZ8Jh8mda+LI7V92hYZf/sKprmOxpxwA= +github.com/koron/go-ssdp v0.9.1/go.mod h1:C43c047jWkDaeg9YuZlSh/QGqOieuWV6dbhWi/jcaLk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= +github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= +github.com/libp2p/go-libp2p v0.49.0 h1:ibXuYPIHmMIPShob1BktQvSuFQkq/MemhQOLKfGujjw= +github.com/libp2p/go-libp2p v0.49.0/go.mod h1:lzjVcOBk5fCn1QD2XbSOKLZesB6gEsry8SLjCsAAGT4= +github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= +github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-pubsub v0.17.0 h1:SNdvB6V0eYMXLRR95n+4vpxJKbFsbHhgjPdDiTpGoo0= +github.com/libp2p/go-libp2p-pubsub v0.17.0/go.mod h1:F0oKCGLFJNy9b0TyRi04b+LchEzq0t2eZyJuxwAIyDE= +github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= +github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= +github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= +github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= +github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= +github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= +github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= +github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= +github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= +github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/marcopolo/simnet v0.0.7 h1:DpH8BMGsF9+1w13L8rvCaAhb6nYJdY+dIXncDrssvUs= +github.com/marcopolo/simnet v0.0.7/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= +github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= +github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= +github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= +github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= +github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= +github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.94 h1:1ZoksIKPyaSt64AVOyaQvhDOgVC3MfZsWM6mZXRUGtM= +github.com/minio/minio-go/v7 v7.0.94/go.mod h1:71t2CqDt3ThzESgZUlU1rBN54mksGGlkLcFgguDnnAc= +github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= +github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= +github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= +github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multiaddr-dns v0.6.0 h1:yKIW08WJHSPJ8bDAT2O/5fypCaUu9Bjl8r/1eJ4XAW8= +github.com/multiformats/go-multiaddr-dns v0.6.0/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0= +github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= +github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= +github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= +github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= +github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= +github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= +github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= +github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc= +github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.21 h1:3yrOwmZFyUpcIosNcWRpQaU+UXIJ6yxLuJ8Bx0mw37Y= +github.com/pion/rtp v1.8.21/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI= +github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8= +github.com/pion/srtp/v3 v3.0.7 h1:QUElw0A/FUg3MP8/KNMZB3i0m8F9XeMnTum86F7S4bs= +github.com/pion/srtp/v3 v3.0.7/go.mod h1:qvnHeqbhT7kDdB+OGB05KA/P067G3mm7XBfLaLiaNF0= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw= +github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= +github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= +github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= +github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= +github.com/pion/webrtc/v4 v4.1.4 h1:/gK1ACGHXQmtyVVbJFQDxNoODg4eSRiFLB7t9r9pg8M= +github.com/pion/webrtc/v4 v4.1.4/go.mod h1:Oab9npu1iZtQRMic3K3toYq5zFPvToe/QBw7dMI2ok4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polydawn/refmt v0.90.0 h1:58BfEsP+G4uIRD9ApJTFsag+Mw+QQlZuH9uI/lPmjfY= +github.com/polydawn/refmt v0.90.0/go.mod h1:XAlDMOunevTYDsZtOKQd8itHXFMsX/QtDkPHaj6ZLxk= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= +github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= +github.com/quic-go/webtransport-go v0.11.1 h1:rrFQMO+7/52ZDJ04fsrjIaWqn6q1z1MYo9iVFq6JtbA= +github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351Pww2leWV3SwlTovkA= +github.com/redis/go-redis/extra/rediscmd/v9 v9.22.0 h1:MQPzEEnpD0BMPufBLABnMYLJVwM7xi7vZ+srO8Nr0s8= +github.com/redis/go-redis/extra/rediscmd/v9 v9.22.0/go.mod h1:eve0JFcLRwFVj3RA85rrrV5+UJ+K9LDyU7kf2UdSueM= +github.com/redis/go-redis/extra/redisotel/v9 v9.22.0 h1:t5ul1Gl0o1rYQj5f5bK12G9xcg1niq2ON4yZFjvy1kA= +github.com/redis/go-redis/extra/redisotel/v9 v9.22.0/go.mod h1:hcS9L2RBBjYXkrfSOF26ZGejgo+yOC+28ZD2fkk3sGs= +github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0= +github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +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/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= +github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +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.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI= +github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE= +github.com/testcontainers/testcontainers-go/modules/minio v0.44.0 h1:pL6fNLanz9f/IhVr50U877HcAB1Zyfa+7Wo182K6T6o= +github.com/testcontainers/testcontainers-go/modules/minio v0.44.0/go.mod h1:tE8z7l7xrs6QZgyM28lryrAdQhfT66kBA0fFUni7rPc= +github.com/testcontainers/testcontainers-go/modules/valkey v0.44.0 h1:knKD2DGjhwV4tWgIalmSFyhkFCCpzH+U0lsxrsgJEoo= +github.com/testcontainers/testcontainers-go/modules/valkey v0.44.0/go.mod h1:Rx2WWz5UBBJSkv2mC81iYnIxCe29SF0DkD7PhWnovXQ= +github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= +github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= +github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= +github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= +github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= +github.com/valkey-io/valkey-go v1.0.59 h1:W67Z0UY+Qqk3k8NKkFCFlM3X4yQUniixl7dSJAch2Qo= +github.com/valkey-io/valkey-go v1.0.59/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/detectors/aws/ecs v1.46.0 h1:pfvInCLWaJ0r6LXDaNbv8T8YUiOcxb2AWitj96/8vJA= +go.opentelemetry.io/contrib/detectors/aws/ecs v1.46.0/go.mod h1:JbeaTWB9iZIrNr5kKAosyBCcgQlM37Z6unN5dUM1+fk= +go.opentelemetry.io/contrib/detectors/aws/lambda v0.71.0 h1:K/vw/nWl3oR6vozjJK5k/+GgFpWFr2JXRkTl7FxZBMM= +go.opentelemetry.io/contrib/detectors/aws/lambda v0.71.0/go.mod h1:7hItUtZBs91nLggxMy3KjoOpCkqccYF7OJWJU6utduA= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda v0.71.0 h1:gN8Fv5fUr6KBhUKL1FbrJ2XzyI/u1UyumfoWX9rkI7s= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda v0.71.0/go.mod h1:VYIezIba9yhDnbnkDs1y+oR4NX7p7VO4lzAXWnpmeAM= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.71.0 h1:ZiBz2gzZi+NwBk5T5X0Myv9lJl44Pwfn6pTGrml/1fU= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.71.0/go.mod h1:aooSSF40vZQZ+AVWv95T2eVU5ZZWiPgqrTtBgaOWxgg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 h1:LMuyCAyfalSjDyjdC65nK6N0zoTT63+E/u95X0JovZI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0/go.mod h1:085m8qbm4hgc8rZWGDEa4vmyyo2c3nPxUslYUKUIU04= +go.opentelemetry.io/contrib/propagators/aws v1.46.0 h1:JslT1wq/5vb6lQsbdOqShvIEs7sDlf0IvKxNZdadfjY= +go.opentelemetry.io/contrib/propagators/aws v1.46.0/go.mod h1:JE4srRJf2cRJcJjRaNhViFjyFJqiCZJiDVlqe6GWXsA= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= +go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= +go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= +go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE= +go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= +go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk= +go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= +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.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= +go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= +go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260717140457-bdb89881bb75 h1:I9ygRooEYoVHV0SRNOSr/KVjTf5EeJ52BuNkVjsP2GU= +golang.org/x/telemetry v0.0.0-20260717140457-bdb89881bb75/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc= +google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +pitr.ca/jsontokenizer v0.3.2 h1:kzsKwfkWPV5XCmAF//hznh3L9IXbiL8aHbI0FswRkCs= +pitr.ca/jsontokenizer v0.3.2/go.mod h1:3DJdA2QNOU6cI0XkH6pRKZ4Oe8G5SDRUQ6PFAwaQ3YY= diff --git a/indexing-service/pkg/aws/dynamoprovidercontexttable.go b/indexing-service/pkg/aws/dynamoprovidercontexttable.go new file mode 100644 index 00000000..bd0d1157 --- /dev/null +++ b/indexing-service/pkg/aws/dynamoprovidercontexttable.go @@ -0,0 +1,103 @@ +package aws + +import ( + "context" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/fil-forge/go-ipni-tools/pkg/store" + "github.com/libp2p/go-libp2p/core/peer" +) + +// ErrDynamoRecordNotFound is used when there is no record in a dynamo table +// (given that GetItem does not actually error) +var ErrDynamoRecordNotFound = errors.New("no record found in dynamo table") + +// DynamoProviderContextTable implements the store.ProviderContextTable interface on dynamodb +type DynamoProviderContextTable struct { + tableName string + dynamoDbClient *dynamodb.Client +} + +var _ store.ProviderContextTable = (*DynamoProviderContextTable)(nil) + +// NewDynamoProviderContextTable returns a ProviderContextTable connected to a AWS DynamoDB table +func NewDynamoProviderContextTable(cfg aws.Config, tableName string) *DynamoProviderContextTable { + return &DynamoProviderContextTable{ + tableName: tableName, + dynamoDbClient: dynamodb.NewFromConfig(cfg), + } +} + +// Delete implements store.ProviderContextTable. +func (d *DynamoProviderContextTable) Delete(ctx context.Context, p peer.ID, contextID []byte) error { + providerContextItem := providerContextItem{p.String(), contextID, nil} + _, err := d.dynamoDbClient.DeleteItem(ctx, &dynamodb.DeleteItemInput{ + TableName: aws.String(d.tableName), Key: providerContextItem.GetKey(), + }) + return err +} + +// Get implements store.ProviderContextTable. +func (d *DynamoProviderContextTable) Get(ctx context.Context, p peer.ID, contextID []byte) ([]byte, error) { + providerContextItem := providerContextItem{p.String(), contextID, nil} + response, err := d.dynamoDbClient.GetItem(ctx, &dynamodb.GetItemInput{ + Key: providerContextItem.GetKey(), + TableName: aws.String(d.tableName), + ProjectionExpression: aws.String("contents"), + }) + if err != nil { + return nil, fmt.Errorf("retrieving item: %w", err) + } + if response.Item == nil { + return nil, store.NewErrNotFound(ErrDynamoRecordNotFound) + } + err = attributevalue.UnmarshalMap(response.Item, &providerContextItem) + if err != nil { + return nil, fmt.Errorf("deserializing item: %w", err) + } + return providerContextItem.Contents, nil +} + +// Put implements store.ProviderContextTable. +func (d *DynamoProviderContextTable) Put(ctx context.Context, p peer.ID, contextID []byte, data []byte) error { + item, err := attributevalue.MarshalMap(providerContextItem{ + Provider: p.String(), + ContextID: contextID, + Contents: data, + }) + if err != nil { + return fmt.Errorf("serializing item: %w", err) + } + _, err = d.dynamoDbClient.PutItem(ctx, &dynamodb.PutItemInput{ + TableName: aws.String(d.tableName), Item: item, + }) + if err != nil { + return fmt.Errorf("storing item: %w", err) + } + return nil +} + +type providerContextItem struct { + Provider string `dynamodbav:"provider"` + ContextID []byte `dynamodbav:"contextID"` + Contents []byte `dynamodbav:"contents"` +} + +// GetKey returns the composite primary key of the provider & contextID in a format that can be +// sent to DynamoDB. +func (p providerContextItem) GetKey() map[string]types.AttributeValue { + provider, err := attributevalue.Marshal(p.Provider) + if err != nil { + panic(err) + } + contextID, err := attributevalue.Marshal(p.ContextID) + if err != nil { + panic(err) + } + return map[string]types.AttributeValue{"provider": provider, "contextID": contextID} +} diff --git a/indexing-service/pkg/aws/iamauthtokenrequest.go b/indexing-service/pkg/aws/iamauthtokenrequest.go new file mode 100644 index 00000000..5909e756 --- /dev/null +++ b/indexing-service/pkg/aws/iamauthtokenrequest.go @@ -0,0 +1,109 @@ +package aws + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + signer "github.com/aws/aws-sdk-go-v2/aws/signer/v4" +) + +// Taken from: https://github.com/redis/go-redis/discussions/2343 + +const ( + REQUEST_PROTOCOL = "http://" + PARAM_ACTION = "Action" + PARAM_USER = "User" + ACTION_NAME = "connect" + SERVICE_NAME = "elasticache" + PARAM_EXPIRES = "X-Amz-Expires" + TOKEN_EXPIRY_SECONDS = 899 + + EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // the hex encoded SHA-256 of an empty string +) + +type IAMAuthTokenRequest struct { + userID string + cacheName string + region string +} + +func (i *IAMAuthTokenRequest) toSignedRequestUri(ctx context.Context, credential aws.Credentials) (string, error) { + req, err := i.getSignableRequest() + if err != nil { + return "", err + } + + signedURI, _, err := i.sign(ctx, req, credential) + if err != nil { + return "", err + } + + u, err := url.Parse(signedURI) + if err != nil { + return "", err + } + + res := url.URL{ + Scheme: "http", + Host: u.Host, + Path: "/", + RawQuery: u.RawQuery, + } + + return strings.Replace(res.String(), REQUEST_PROTOCOL, "", 1), nil +} + +func (i *IAMAuthTokenRequest) getSignableRequest() (*http.Request, error) { + query := url.Values{ + PARAM_ACTION: {ACTION_NAME}, + PARAM_USER: {i.userID}, + PARAM_EXPIRES: {strconv.FormatInt(int64(TOKEN_EXPIRY_SECONDS), 10)}, + } + + signURL := url.URL{ + Scheme: "http", + Host: i.cacheName, + Path: "/", + RawQuery: query.Encode(), + } + + req, err := http.NewRequest(http.MethodGet, signURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (i *IAMAuthTokenRequest) sign(ctx context.Context, req *http.Request, credential aws.Credentials) (signedURI string, signedHeaders http.Header, err error) { + s := signer.NewSigner() + return s.PresignHTTP(ctx, credential, req, EMPTY_BODY_SHA256, SERVICE_NAME, i.region, time.Now()) +} + +func redisCredentialVerifier(cfg aws.Config, userID string, cacheName string) func(context.Context) (string, string, error) { + return func(ctx context.Context) (string, string, error) { + iamAuthTokenRequest := IAMAuthTokenRequest{ + userID: userID, + cacheName: cacheName, + region: cfg.Region, + } + + credentials, err := cfg.Credentials.Retrieve(ctx) + if err != nil { + return "", "", fmt.Errorf("getting aws credentials: %w", err) + } + iamAuthToken, err := iamAuthTokenRequest.toSignedRequestUri(ctx, credentials) + + if err != nil { + return "", "", fmt.Errorf("attempting to obtain signed redis loging: %w", err) + } + + return userID, iamAuthToken, nil + } +} diff --git a/indexing-service/pkg/aws/s3store.go b/indexing-service/pkg/aws/s3store.go new file mode 100644 index 00000000..da953ccb --- /dev/null +++ b/indexing-service/pkg/aws/s3store.go @@ -0,0 +1,116 @@ +package aws + +import ( + "context" + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "io" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/smithy-go" + "github.com/fil-forge/go-ipni-tools/pkg/store" +) + +// S3Store implements the store.Store interface on S3 +type S3Store struct { + bucket string + keyPrefix string + s3Client *s3.Client +} + +var _ store.Store = (*S3Store)(nil) + +// Get implements store.Store. +func (s *S3Store) Get(ctx context.Context, key string) (io.ReadCloser, error) { + outPut, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(s.keyPrefix + key), + }) + if err != nil { + var noSuchKeyError *types.NoSuchKey + // wrap in error recognizable as a not found error for Store interface consumers + if errors.As(err, &noSuchKeyError) { + return nil, store.NewErrNotFound(err) + } + return nil, err + } + return outPut.Body, nil +} + +// Put implements store.Store. +func (s *S3Store) Put(ctx context.Context, key string, len uint64, data io.Reader) error { + _, err := s.s3Client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(s.keyPrefix + key), + Body: data, + ContentLength: aws.Int64(int64(len)), + }) + return err +} + +func (s *S3Store) Replace(ctx context.Context, key string, old io.Reader, length uint64, new io.Reader) error { + input := s3.PutObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(s.keyPrefix + key), + Body: new, + ContentLength: aws.Int64(int64(length)), + } + + // use conditional write requests to replace the data + if old == nil { + input.IfNoneMatch = aws.String("*") + } else { + b, err := io.ReadAll(old) + if err != nil { + return err + } + md5hash := md5.Sum(b) + etag := fmt.Sprintf("%q", hex.EncodeToString(md5hash[:])) + input.IfMatch = aws.String(etag) + } + + _, err := s.s3Client.PutObject(ctx, &input) + if err != nil { + var oe smithy.APIError + if errors.As(err, &oe) { + // This method is used by the IPNI publisher to write a new head to the + // chain. We can receive one of the following error code because we're + // using `If-Match`. + // + // PreconditionFailed: At least one of the preconditions you specified did + // not hold. + // + // OperationAborted: A conflicting conditional action is currently in + // progress against this resource. Try again. + // + // If we receive OperationAborted then we'd get PreconditionFailed on the + // next try, since we don't put the same head twice, and so there is no + // chance to succeed with the same If-Match Etag, since the content will + // have changed due to the "conflicting conditional action" already in + // progress. Hence we don't try again and simply return the error + // [store.ErrPreconditionFailed] so that the calling code can try again. + // + // When ErrPreconditionFailed is returned, a new advert must be + // constructed that references the new head and the operation retried. + if oe.ErrorCode() == "PreconditionFailed" || oe.ErrorCode() == "OperationAborted" { + return store.ErrPreconditionFailed + } + } + } + return err +} + +func NewS3StoreWithClient(client *s3.Client, bucket string, keyPrefix string) *S3Store { + return &S3Store{s3Client: client, bucket: bucket, keyPrefix: keyPrefix} +} + +func NewS3Store(cfg aws.Config, bucket string, keyPrefix string) *S3Store { + client := s3.NewFromConfig(cfg, func(options *s3.Options) { + options.DisableLogOutputChecksumValidationSkipped = true + }) + return NewS3StoreWithClient(client, bucket, keyPrefix) +} diff --git a/indexing-service/pkg/aws/s3store_test.go b/indexing-service/pkg/aws/s3store_test.go new file mode 100644 index 00000000..3a952fdf --- /dev/null +++ b/indexing-service/pkg/aws/s3store_test.go @@ -0,0 +1,98 @@ +package aws_test + +import ( + "bytes" + "encoding/hex" + "net/url" + "os" + "runtime" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + iaws "github.com/fil-forge/forge/indexing-service/pkg/aws" + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/go-ipni-tools/pkg/store" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/minio" +) + +func TestS3StoreReplace(t *testing.T) { + if os.Getenv("CI") != "" && runtime.GOOS != "linux" { + t.SkipNow() + } + + endpoint := createS3(t) + client := newS3Client(t, endpoint) + + bucketName := hex.EncodeToString(testutil.RandomBytes(t, 16)) + createBucket(t, client, bucketName) + + st := iaws.NewS3StoreWithClient(client, bucketName, "") + + t.Run("conditional write", func(t *testing.T) { + key := hex.EncodeToString(testutil.RandomBytes(t, 4)) + first := testutil.RandomBytes(t, 32) + second := testutil.RandomBytes(t, 32) + third := testutil.RandomBytes(t, 32) + + err := st.Put(t.Context(), key, 32, bytes.NewReader(first)) + require.NoError(t, err) + + err = st.Replace(t.Context(), key, bytes.NewReader(first), 32, bytes.NewReader(second)) + require.NoError(t, err) + + err = st.Replace(t.Context(), key, bytes.NewReader(first), 32, bytes.NewReader(third)) + require.ErrorIs(t, err, store.ErrPreconditionFailed) + }) + + t.Run("conditional first write", func(t *testing.T) { + key := hex.EncodeToString(testutil.RandomBytes(t, 4)) + first := testutil.RandomBytes(t, 32) + + err := st.Replace(t.Context(), key, nil, 32, bytes.NewReader(first)) + require.NoError(t, err) + }) +} + +func createS3(t *testing.T) *url.URL { + container, err := minio.Run(t.Context(), "minio/minio:latest") + testcontainers.CleanupContainer(t, container) + require.NoError(t, err) + + addr, err := container.ConnectionString(t.Context()) + require.NoError(t, err) + + return testutil.Must(url.Parse("http://" + addr))(t) +} + +func newS3Client(t *testing.T, endpoint *url.URL) *s3.Client { + cfg, err := config.LoadDefaultConfig( + t.Context(), + config.WithCredentialsProvider(credentials.StaticCredentialsProvider{ + Value: aws.Credentials{ + AccessKeyID: "minioadmin", + SecretAccessKey: "minioadmin", + }, + }), + func(o *config.LoadOptions) error { + o.Region = "us-east-1" + return nil + }, + ) + require.NoError(t, err) + + return s3.NewFromConfig(cfg, func(o *s3.Options) { + base := endpoint.String() + o.BaseEndpoint = &base + o.UsePathStyle = true + }) +} + +func createBucket(t *testing.T, client *s3.Client, name string) { + _, err := client.CreateBucket(t.Context(), &s3.CreateBucketInput{Bucket: aws.String(name)}) + require.NoError(t, err) +} diff --git a/indexing-service/pkg/aws/service.go b/indexing-service/pkg/aws/service.go new file mode 100644 index 00000000..a4af7a6f --- /dev/null +++ b/indexing-service/pkg/aws/service.go @@ -0,0 +1,351 @@ +package aws + +import ( + "context" + crypto_ed25519 "crypto/ed25519" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "maps" + "net/url" + "os" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/fil-forge/forge/indexing-service/pkg/build" + "github.com/fil-forge/forge/indexing-service/pkg/construct" + "github.com/fil-forge/forge/indexing-service/pkg/presets" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + publisherqueue "github.com/fil-forge/go-ipni-tools/pkg/queue" + "github.com/fil-forge/go-ipni-tools/pkg/store" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/did/resolver" + "github.com/fil-forge/ucantone/multikey" + "github.com/fil-forge/ucantone/multikey/ed25519" + "github.com/getsentry/sentry-go" + logging "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/core/crypto" + goredis "github.com/redis/go-redis/v9" +) + +// ErrNoPrivateKey means that the value returned from Secrets was empty +var ErrNoPrivateKey = errors.New("no value for private key") + +func mustGetEnv(envVar string) string { + value := os.Getenv(envVar) + if len(value) == 0 { + panic(fmt.Errorf("missing env var: %s", envVar)) + } + return value +} + +func mustGetInt(envVar string) int64 { + stringValue := mustGetEnv(envVar) + value, err := strconv.ParseInt(stringValue, 10, 64) + if err != nil { + panic(fmt.Errorf("parsing env var %s to int: %w", envVar, err)) + } + return value +} + +func mustGetFloat(envVar string) float64 { + stringValue := mustGetEnv(envVar) + value, err := strconv.ParseFloat(stringValue, 64) + if err != nil { + panic(fmt.Errorf("parsing env var %s to int: %w", envVar, err)) + } + return value +} + +// Config describes all the values required to setup AWS from the environment +type Config struct { + construct.ServiceConfig + aws.Config + ProvidersCacheExpirationSeconds int64 + NoProvidersCacheExpirationSeconds int64 + ClaimsCacheExpirationSeconds int64 + IndexesCacheExpirationSeconds int64 + SQSCachingQueueID string + CachingBucket string + SQSPublishingQueueID string + PublishingBucket string + SQSAdvertisementPublishingQueueID string + ChunkLinksTableName string + MetadataTableName string + IPNIStoreBucket string + IPNIStorePrefix string + IPNIAnnounceURLs []url.URL + NotifierHeadBucket string + NotifierTopicArn string + ClaimStoreBucket string + ClaimStorePrefix string + BaseTraceSampleRatio float64 + SentryDSN string + SentryEnvironment string + TelemetryEnabled bool + PrincipalMapping map[string]string + IPNIFormatPeerID string + IPNIFormatEndpoint string + Issuer multikey.Issuer +} + +// FromEnv constructs the AWS Configuration from the environment +func FromEnv(ctx context.Context) Config { + awsConfig, err := config.LoadDefaultConfig(ctx) + if err != nil { + panic(fmt.Errorf("loading aws default config: %w", err)) + } + + idSigner, err := ed25519.Parse(mustGetEnv("PRIVATE_KEY")) + if err != nil { + panic(fmt.Errorf("parsing private key: %s", err)) + } + + var id multikey.Issuer + id = multikey.KeyIssuer(idSigner) + if len(os.Getenv("DID")) != 0 { + d, err := did.Parse(os.Getenv("DID")) + if err != nil { + panic(fmt.Errorf("parsing DID: %w", err)) + } + id = multikey.NewIssuer(d, idSigner) + } + + // id.Raw() returns the 32-byte seed; libp2p's UnmarshalEd25519PrivateKey + // wants the 64-byte stdlib form (seed||pub). Expand via NewKeyFromSeed. + cryptoPrivKey, err := crypto.UnmarshalEd25519PrivateKey(crypto_ed25519.NewKeyFromSeed(idSigner.Raw())) + if err != nil { + panic(fmt.Errorf("unmarshaling private key: %w", err)) + } + + ipniStoreKeyPrefix := os.Getenv("IPNI_STORE_KEY_PREFIX") + if len(ipniStoreKeyPrefix) == 0 { + ipniStoreKeyPrefix = "ipni/v1/ad/" + } + + ipniPublisherAnnounceAddress := fmt.Sprintf("/dns/%s/https", mustGetEnv("IPNI_STORE_BUCKET_REGIONAL_DOMAIN")) + + var principalMapping map[string]string + if os.Getenv("PRINCIPAL_MAPPING") != "" { + principalMapping = map[string]string{} + maps.Copy(principalMapping, presets.PrincipalMapping) + var pm map[string]string + err := json.Unmarshal([]byte(os.Getenv("PRINCIPAL_MAPPING")), &pm) + if err != nil { + panic(fmt.Errorf("parsing principal mapping: %w", err)) + } + maps.Copy(principalMapping, pm) + } else { + principalMapping = presets.PrincipalMapping + } + + ipniFindURL := os.Getenv("IPNI_ENDPOINT") + if ipniFindURL == "" { + ipniFindURL = presets.IPNIFindURL + } + + var ipniFindFallbackURLs []string + if os.Getenv("IPNI_FALLBACK_ENDPOINTS") != "" { + err := json.Unmarshal([]byte(os.Getenv("IPNI_FALLBACK_ENDPOINTS")), &ipniFindFallbackURLs) + if err != nil { + panic(fmt.Errorf("parsing IPNI fallback endpoints JSON: %w", err)) + } + } + + var ipniPublisherDirectAnnounceURLs []string + if os.Getenv("IPNI_ANNOUNCE_URLS") != "" { + err := json.Unmarshal([]byte(os.Getenv("IPNI_ANNOUNCE_URLS")), &ipniPublisherDirectAnnounceURLs) + if err != nil { + panic(fmt.Errorf("parsing IPNI announce URLs JSON: %w", err)) + } + } else { + ipniPublisherDirectAnnounceURLs = presets.IPNIAnnounceURLs + } + + return Config{ + Config: awsConfig, + Issuer: id, + ServiceConfig: construct.ServiceConfig{ + ID: id, + PrivateKey: cryptoPrivKey, + PublicURL: strings.Split(mustGetEnv("PUBLIC_URL"), ","), + ProvidersRedis: goredis.ClusterOptions{ + Addrs: []string{mustGetEnv("PROVIDERS_CACHE_URL")}, + ReadOnly: true, + RouteRandomly: true, + CredentialsProviderContext: redisCredentialVerifier(awsConfig, mustGetEnv("CACHE_USER_ID"), mustGetEnv("PROVIDERS_CACHE_ID")), + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + }, + NoProviderRedis: goredis.ClusterOptions{ + Addrs: []string{mustGetEnv("NO_PROVIDERS_CACHE_URL")}, + ReadOnly: true, + RouteRandomly: true, + CredentialsProviderContext: redisCredentialVerifier(awsConfig, mustGetEnv("CACHE_USER_ID"), mustGetEnv("NO_PROVIDERS_CACHE_ID")), + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + }, + ClaimsRedis: goredis.ClusterOptions{ + Addrs: []string{mustGetEnv("CLAIMS_CACHE_URL")}, + ReadOnly: true, + RouteRandomly: true, + CredentialsProviderContext: redisCredentialVerifier(awsConfig, mustGetEnv("CACHE_USER_ID"), mustGetEnv("CLAIMS_CACHE_ID")), + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + }, + IndexesRedis: goredis.ClusterOptions{ + Addrs: []string{mustGetEnv("INDEXES_CACHE_URL")}, + ReadOnly: true, + RouteRandomly: true, + CredentialsProviderContext: redisCredentialVerifier(awsConfig, mustGetEnv("CACHE_USER_ID"), mustGetEnv("INDEXES_CACHE_ID")), + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + }, + IPNIFindURL: ipniFindURL, + IPNIFindFallbackURLs: ipniFindFallbackURLs, + IPNIAnnounceAddrs: []string{ipniPublisherAnnounceAddress}, + IPNIDirectAnnounceURLs: ipniPublisherDirectAnnounceURLs, + }, + ProvidersCacheExpirationSeconds: mustGetInt("PROVIDERS_CACHE_EXPIRATION_SECONDS"), + NoProvidersCacheExpirationSeconds: mustGetInt("NO_PROVIDERS_CACHE_EXPIRATION_SECONDS"), + ClaimsCacheExpirationSeconds: mustGetInt("CLAIMS_CACHE_EXPIRATION_SECONDS"), + IndexesCacheExpirationSeconds: mustGetInt("INDEXES_CACHE_EXPIRATION_SECONDS"), + SQSCachingQueueID: mustGetEnv("PROVIDER_CACHING_QUEUE_ID"), + CachingBucket: mustGetEnv("PROVIDER_CACHING_BUCKET_NAME"), + SQSPublishingQueueID: mustGetEnv("IPNI_PUBLISHER_QUEUE_ID"), + PublishingBucket: mustGetEnv("IPNI_PUBLISHER_BUCKET_NAME"), + SQSAdvertisementPublishingQueueID: mustGetEnv("IPNI_ADVERTISEMENT_PUBLISHING_QUEUE_ID"), + ChunkLinksTableName: mustGetEnv("CHUNK_LINKS_TABLE_ID"), + MetadataTableName: mustGetEnv("METADATA_TABLE_ID"), + IPNIStoreBucket: mustGetEnv("IPNI_STORE_BUCKET_NAME"), + IPNIStorePrefix: ipniStoreKeyPrefix, + NotifierHeadBucket: mustGetEnv("NOTIFIER_HEAD_BUCKET_NAME"), + ClaimStoreBucket: mustGetEnv("CLAIM_STORE_BUCKET_NAME"), + ClaimStorePrefix: os.Getenv("CLAIM_STORE_KEY_PREFIX"), + BaseTraceSampleRatio: mustGetFloat("BASE_TRACE_SAMPLE_RATIO"), + SentryDSN: os.Getenv("SENTRY_DSN"), + SentryEnvironment: os.Getenv("SENTRY_ENVIRONMENT"), + TelemetryEnabled: os.Getenv("TELEMETRY_DISABLED") == "", + IPNIFormatPeerID: os.Getenv("IPNI_FORMAT_PEER_ID"), + IPNIFormatEndpoint: os.Getenv("IPNI_FORMAT_ENDPOINT"), + PrincipalMapping: principalMapping, + } +} + +// Construct constructs types.Service from AWS deps for Lamda functions +func Construct(cfg Config) (types.Service, error) { + httpClient := construct.DefaultHTTPClient() + providersClient := goredis.NewClusterClient(&cfg.ProvidersRedis) + noProvidersClient := goredis.NewClusterClient(&cfg.NoProviderRedis) + claimsClient := goredis.NewClusterClient(&cfg.ClaimsRedis) + indexesClient := goredis.NewClusterClient(&cfg.IndexesRedis) + + // instrument HTTP and redis clients if telemetry is enabled + if cfg.TelemetryEnabled { + httpClient = telemetry.InstrumentHTTPClient(construct.DefaultHTTPClient()) + providersClient = telemetry.InstrumentRedisClient(providersClient) + noProvidersClient = telemetry.InstrumentRedisClient(noProvidersClient) + claimsClient = telemetry.InstrumentRedisClient(claimsClient) + indexesClient = telemetry.InstrumentRedisClient(indexesClient) + } + + cachingQueue := NewSQSCachingQueue(cfg.Config, cfg.SQSCachingQueueID, cfg.CachingBucket) + ipniStore := NewS3Store(cfg.Config, cfg.IPNIStoreBucket, cfg.IPNIStorePrefix) + claimBucketStore := contentclaims.NewStoreFromBucket(NewS3Store(cfg.Config, cfg.ClaimStoreBucket, cfg.ClaimStorePrefix)) + chunkLinksTable := NewDynamoProviderContextTable(cfg.Config, cfg.ChunkLinksTableName) + metadataTable := NewDynamoProviderContextTable(cfg.Config, cfg.MetadataTableName) + publisherStore := store.NewPublisherStore(ipniStore, chunkLinksTable, metadataTable, store.WithMetadataContext(metadata.MetadataContext)) + + publishingQueue := NewSQSPublishingQueue(cfg.Config, cfg.SQSPublishingQueueID, cfg.PublishingBucket) + queuePublisher := publisherqueue.NewQueuePublisher(publishingQueue) + var provIndexLog logging.EventLogger + if cfg.SentryDSN != "" && cfg.SentryEnvironment != "" { + err := sentry.Init(sentry.ClientOptions{ + Dsn: cfg.SentryDSN, + Environment: cfg.SentryEnvironment, + Release: build.Version, + Transport: sentry.NewHTTPSyncTransport(), + EnableTracing: false, + }) + if err != nil { + return nil, fmt.Errorf("initializing sentry: %w", err) + } + provIndexLog = telemetry.NewSentryLogger("providerindex") + } + opts := []construct.Option{ + construct.SkipNotification(), + construct.WithCachingQueue(cachingQueue), + construct.WithPublisherStore(publisherStore), + construct.WithAsyncPublisher(queuePublisher), + construct.WithStartIPNIServer(false), + construct.WithClaimsStore(claimBucketStore), + construct.WithHTTPClient(httpClient), + construct.WithProvidersClient(redis.NewClusterClientAdapter(providersClient)), + construct.WithNoProvidersClient(noProvidersClient), + construct.WithClaimsClient(claimsClient), + construct.WithIndexesClient(indexesClient), + construct.WithProvidersCacheOptions(redis.ExpirationTime(time.Duration(cfg.ProvidersCacheExpirationSeconds) * time.Second)), + construct.WithNoProvidersCacheOptions(redis.ExpirationTime(time.Duration(cfg.NoProvidersCacheExpirationSeconds) * time.Second)), + construct.WithClaimsCacheOptions(redis.ExpirationTime(time.Duration(cfg.ClaimsCacheExpirationSeconds) * time.Second)), + construct.WithIndexesCacheOptions(redis.ExpirationTime(time.Duration(cfg.IndexesCacheExpirationSeconds) * time.Second)), + construct.WithProviderIndexLogger(provIndexLog), + } + + service, err := construct.Construct( + cfg.ServiceConfig, + opts..., + ) + if err != nil { + return nil, err + } + + return service, nil +} + +func NewPrincipalMappingResolver(mapping map[string]string) (resolver.WellKnown, error) { + resolver := resolver.WellKnown{} + for didStr, verifierStr := range mapping { + d, err := did.Parse(didStr) + if err != nil { + return nil, fmt.Errorf("parsing DID %q for principal mapping: %w", didStr, err) + } + + // Deprecated + if verifierStr[:8] == "did:key:" { + verifierStr = verifierStr[8:] + } + + ver, err := multikey.Parse(verifierStr) + if err != nil { + return nil, fmt.Errorf("parsing verifier %q for principal mapping: %w", verifierStr, err) + } + + doc := did.NewDocument(d) + vm := multikey.DeriveVerificationMethod(doc.Fragment("key"), ver) + + if err := doc.VerificationMethods.Add(vm); err != nil { + return nil, err + } + if err := doc.CapabilityDelegation.Add(vm); err != nil { + return nil, err + } + if err := doc.CapabilityInvocation.Add(vm); err != nil { + return nil, err + } + + resolver[d] = doc + } + return resolver, nil +} diff --git a/indexing-service/pkg/aws/snsremotesyncnotifer.go b/indexing-service/pkg/aws/snsremotesyncnotifer.go new file mode 100644 index 00000000..fff2ca59 --- /dev/null +++ b/indexing-service/pkg/aws/snsremotesyncnotifer.go @@ -0,0 +1,43 @@ +package aws + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sns" + logging "github.com/ipfs/go-log/v2" + "github.com/ipld/go-ipld-prime" +) + +var log = logging.Logger("aws") + +type SNSRemoteSyncMessage struct { + Head string `json:"Head,omitempty"` + Prev string `json:"Prev,omitempty"` +} + +type SNSRemoteSyncNotifier struct { + topicArn string + snsClient *sns.Client +} + +func NewSNSRemoteSyncNotifier(config aws.Config, topicArn string) *SNSRemoteSyncNotifier { + return &SNSRemoteSyncNotifier{ + snsClient: sns.NewFromConfig(config), + topicArn: topicArn, + } +} +func (s *SNSRemoteSyncNotifier) NotifyRemoteSync(ctx context.Context, head, prev ipld.Link) { + messageJSON, err := json.Marshal(SNSRemoteSyncMessage{ + Head: head.String(), + Prev: prev.String(), + }) + if err != nil { + log.Errorf("serializing remote sync message: %s", err.Error()) + } + _, err = s.snsClient.Publish(ctx, &sns.PublishInput{TopicArn: aws.String(s.topicArn), Message: aws.String(string(messageJSON))}) + if err != nil { + log.Errorf("serializing remote sync message: %s", err.Error()) + } +} diff --git a/indexing-service/pkg/aws/sqsadvertisementpublishingqueue.go b/indexing-service/pkg/aws/sqsadvertisementpublishingqueue.go new file mode 100644 index 00000000..e0edc737 --- /dev/null +++ b/indexing-service/pkg/aws/sqsadvertisementpublishingqueue.go @@ -0,0 +1,167 @@ +package aws + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/fil-forge/go-ipni-tools/pkg/queue" + "github.com/google/uuid" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/node/bindnode" + ipldschema "github.com/ipld/go-ipld-prime/schema" + "github.com/ipni/go-libipni/ingest/schema" +) + +type advMarshaller schema.Advertisement + +func (a advMarshaller) MarshalJSON() ([]byte, error) { + return encodeJSON((*schema.Advertisement)(&a), schema.AdvertisementPrototype.Type()) +} + +func (a *advMarshaller) UnmarshalJSON(data []byte) error { + var adv schema.Advertisement + if err := decodeJSON(data, &adv, schema.AdvertisementPrototype.Type()); err != nil { + return err + } + *a = advMarshaller(adv) + return nil +} + +func encodeJSON(val any, typ ipldschema.Type, opts ...bindnode.Option) (bytes []byte, err error) { + defer func() { + if r := recover(); r != nil { + if asStr, ok := r.(string); ok { + err = errors.New(asStr) + } else if asErr, ok := r.(error); ok { + err = asErr + } else { + err = fmt.Errorf("unknown panic encoding JSON: %+v", r) + } + } + }() + bytes, err = ipld.Marshal(dagjson.Encode, val, typ, opts...) + return +} + +func decodeJSON(b []byte, bind any, typ ipldschema.Type, opts ...bindnode.Option) error { + _, err := ipld.Unmarshal(b, dagjson.Decode, bind, typ, opts...) + return err +} + +// queueMessage is the struct that is serialized onto an SQS message queue in JSON +type queueMessage struct { + JobID uuid.UUID `json:"JobID,omitempty"` + Advertisement advMarshaller `json:"Message,omitempty"` +} + +// SQSAdvertisementPublishingQueue implements a queue for publishing advertisements using SQS +type SQSAdvertisementPublishingQueue struct { + queueID string + sqsClient *sqs.Client + decoder *SQSAdvertisementPublishingDecoder +} + +// NewSQSAdvertisementPublishingQueue returns a new SQSAdvertisementPublishingQueue for the given aws config +func NewSQSAdvertisementPublishingQueue(cfg aws.Config, queueID string) *SQSAdvertisementPublishingQueue { + return &SQSAdvertisementPublishingQueue{ + queueID: queueID, + sqsClient: sqs.NewFromConfig(cfg), + decoder: NewSQSAdvertisementPublishingDecoder(), + } +} + +// Queue sends a new advertisement publishing job to the SQS queue. +func (s *SQSAdvertisementPublishingQueue) Queue(ctx context.Context, adv schema.Advertisement) error { + uuid := uuid.New() + messageJSON, err := json.Marshal(queueMessage{ + JobID: uuid, + Advertisement: advMarshaller(adv), + }) + if err != nil { + return fmt.Errorf("serializing message json: %w", err) + } + message := &sqs.SendMessageInput{ + QueueUrl: aws.String(s.queueID), + MessageBody: aws.String(string(messageJSON)), + MessageGroupId: aws.String("default"), + } + _, err = s.sqsClient.SendMessage(ctx, message) + if err != nil { + return fmt.Errorf("enqueueing message: %w", err) + } + return nil +} + +// Read reads up to maxJobs advertisements from the SQS queue. +func (s *SQSAdvertisementPublishingQueue) Read(ctx context.Context, maxJobs int) ([]queue.WithID[schema.Advertisement], error) { + receiveOutput, err := s.sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{ + QueueUrl: aws.String(s.queueID), + MaxNumberOfMessages: int32(maxJobs), + WaitTimeSeconds: 20, // enable long-polling + }) + if err != nil { + return nil, fmt.Errorf("failed to receive messages from SQS: %w", err) + } + + if len(receiveOutput.Messages) == 0 { + return []queue.WithID[schema.Advertisement]{}, nil + } + + ads := make([]queue.WithID[schema.Advertisement], 0, len(receiveOutput.Messages)) + for _, msg := range receiveOutput.Messages { + ad, err := s.decoder.DecodeMessage(ctx, aws.ToString(msg.ReceiptHandle), aws.ToString(msg.Body)) + if err != nil { + return nil, fmt.Errorf("failed to decode message: %w", err) + } + + ads = append(ads, ad) + } + + return ads, nil +} + +func (s *SQSAdvertisementPublishingQueue) Release(ctx context.Context, jobID string) error { + _, err := s.sqsClient.ChangeMessageVisibility(ctx, &sqs.ChangeMessageVisibilityInput{ + QueueUrl: aws.String(s.queueID), + ReceiptHandle: aws.String(jobID), + VisibilityTimeout: 0, + }) + + return err +} + +// Delete deletes a job message from the SQS publisher queue. +func (s *SQSAdvertisementPublishingQueue) Delete(ctx context.Context, jobID string) error { + _, err := s.sqsClient.DeleteMessage(ctx, &sqs.DeleteMessageInput{ + QueueUrl: aws.String(s.queueID), + ReceiptHandle: aws.String(jobID), + }) + return err +} + +// SQSAdvertisementPublishingDecoder provides interfaces for working with advertisements received over SQS +type SQSAdvertisementPublishingDecoder struct { +} + +// NewSQSDecoder returns a new decoder for the given AWS config +func NewSQSAdvertisementPublishingDecoder() *SQSAdvertisementPublishingDecoder { + return &SQSAdvertisementPublishingDecoder{} +} + +// DecodeMessage decodes a provider caching job from the SQS message body, reading the stored index from S3 +func (s *SQSAdvertisementPublishingDecoder) DecodeMessage(ctx context.Context, receiptHandle string, messageBody string) (queue.WithID[schema.Advertisement], error) { + var decodedMsg queueMessage + err := json.Unmarshal([]byte(messageBody), &decodedMsg) + if err != nil { + return queue.WithID[schema.Advertisement]{}, fmt.Errorf("deserializing message: %w", err) + } + return queue.WithID[schema.Advertisement]{ + ID: receiptHandle, + Job: schema.Advertisement(decodedMsg.Advertisement), + }, nil +} diff --git a/indexing-service/pkg/aws/sqscachingqueue.go b/indexing-service/pkg/aws/sqscachingqueue.go new file mode 100644 index 00000000..761f35f0 --- /dev/null +++ b/indexing-service/pkg/aws/sqscachingqueue.go @@ -0,0 +1,187 @@ +package aws + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/fil-forge/forge/indexing-service/pkg/service/providercacher" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/go-ipni-tools/pkg/queue" + "github.com/google/uuid" + "github.com/ipni/go-libipni/find/model" +) + +// cachingQueueMessage is the struct that is serialized onto an SQS message queue in JSON +type cachingQueueMessage struct { + JobID uuid.UUID `json:"JobID,omitempty"` + Provider model.ProviderResult `json:"Provider,omitempty"` +} + +var _ providercacher.CachingQueue = (*SQSCachingQueue)(nil) + +// SQSCachingQueue implements the providercacher.CachingQueue interface using SQS +type SQSCachingQueue struct { + queueID string + bucket string + s3Client *s3.Client + sqsClient *sqs.Client + decoder *SQSCachingDecoder +} + +// NewSQSCachingQueue returns a new SQSCachingQueue for the given aws config +func NewSQSCachingQueue(cfg aws.Config, queueID string, bucket string) *SQSCachingQueue { + return &SQSCachingQueue{ + queueID: queueID, + bucket: bucket, + s3Client: s3.NewFromConfig(cfg), + sqsClient: sqs.NewFromConfig(cfg), + decoder: NewSQSCachingDecoder(cfg, bucket), + } +} + +// Queue implements blobindexlookup.CachingQueue. +func (s *SQSCachingQueue) Queue(ctx context.Context, job providercacher.ProviderCachingJob) error { + uuid := uuid.New() + + var data bytes.Buffer + if err := blobindex.Archive(job.Index, &data); err != nil { + return fmt.Errorf("serializing index to CAR: %w", err) + } + _, err := s.s3Client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(uuid.String()), + Body: bytes.NewReader(data.Bytes()), + ContentLength: aws.Int64(int64(data.Len())), + }) + if err != nil { + return fmt.Errorf("saving index CAR to S3: %w", err) + } + err = s.sendMessage(ctx, cachingQueueMessage{ + JobID: uuid, + Provider: job.Provider, + }) + if err != nil { + // error sending message so cleanup queue + _, s3deleteErr := s.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(uuid.String()), + }) + if s3deleteErr != nil { + err = errors.Join(err, fmt.Errorf("cleaning up index CAR on S3: %w", s3deleteErr)) + } + } + return err +} + +func (s *SQSCachingQueue) sendMessage(ctx context.Context, msg cachingQueueMessage) error { + messageJSON, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("serializing message json: %w", err) + } + _, err = s.sqsClient.SendMessage(ctx, &sqs.SendMessageInput{ + QueueUrl: aws.String(s.queueID), + MessageBody: aws.String(string(messageJSON)), + }) + if err != nil { + return fmt.Errorf("enqueueing message: %w", err) + } + return nil +} + +// Read reads a batch of jobs from the SQS queue. +// Returns an empty slice if no jobs are available. +// The caller must process jobs and delete them from the queue when done. +func (s *SQSCachingQueue) Read(ctx context.Context, maxJobs int) ([]queue.WithID[providercacher.ProviderCachingJob], error) { + receiveOutput, err := s.sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{ + QueueUrl: aws.String(s.queueID), + MaxNumberOfMessages: int32(maxJobs), + WaitTimeSeconds: 20, // enable long-polling + }) + if err != nil { + return nil, fmt.Errorf("failed to receive messages from SQS: %w", err) + } + + if len(receiveOutput.Messages) == 0 { + return []queue.WithID[providercacher.ProviderCachingJob]{}, nil + } + + jobs := make([]queue.WithID[providercacher.ProviderCachingJob], 0, len(receiveOutput.Messages)) + for _, msg := range receiveOutput.Messages { + job, err := s.decoder.DecodeMessage(ctx, aws.ToString(msg.ReceiptHandle), aws.ToString(msg.Body)) + if err != nil { + return nil, fmt.Errorf("failed to decode message: %w", err) + } + + jobs = append(jobs, job) + } + + return jobs, nil +} + +// Release makes a job available for processing again by making it visible in the queue +func (s *SQSCachingQueue) Release(ctx context.Context, jobID string) error { + _, err := s.sqsClient.ChangeMessageVisibility(ctx, &sqs.ChangeMessageVisibilityInput{ + QueueUrl: aws.String(s.queueID), + ReceiptHandle: aws.String(jobID), + VisibilityTimeout: 0, + }) + + return err +} + +// Delete deletes a job message from the SQS queue. +func (s *SQSCachingQueue) Delete(ctx context.Context, jobID string) error { + _, err := s.sqsClient.DeleteMessage(ctx, &sqs.DeleteMessageInput{ + QueueUrl: aws.String(s.queueID), + ReceiptHandle: aws.String(jobID), + }) + return err +} + +// SQSCachingDecoder provides interfaces for working with caching jobs received over SQS +type SQSCachingDecoder struct { + bucket string + s3Client *s3.Client +} + +// NewSQSCachingDecoder returns a new decoder for the given AWS config +func NewSQSCachingDecoder(cfg aws.Config, bucket string) *SQSCachingDecoder { + return &SQSCachingDecoder{ + bucket: bucket, + s3Client: s3.NewFromConfig(cfg), + } +} + +// DecodeMessage decodes a provider caching job from the SQS message body, reading the stored index from S3 +func (s *SQSCachingDecoder) DecodeMessage(ctx context.Context, receiptHandle string, messageBody string) (queue.WithID[providercacher.ProviderCachingJob], error) { + var msg cachingQueueMessage + err := json.Unmarshal([]byte(messageBody), &msg) + if err != nil { + return queue.WithID[providercacher.ProviderCachingJob]{}, fmt.Errorf("deserializing message: %w", err) + } + received, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(msg.JobID.String()), + }) + if err != nil { + return queue.WithID[providercacher.ProviderCachingJob]{}, fmt.Errorf("reading stored index CAR: %w", err) + } + defer received.Body.Close() + index, err := blobindex.Extract(received.Body) + if err != nil { + return queue.WithID[providercacher.ProviderCachingJob]{}, fmt.Errorf("deserializing index: %w", err) + } + return queue.WithID[providercacher.ProviderCachingJob]{ + ID: receiptHandle, + Job: providercacher.ProviderCachingJob{ + Provider: msg.Provider, + Index: index, + }, + }, nil +} diff --git a/indexing-service/pkg/aws/sqspublishingqueue.go b/indexing-service/pkg/aws/sqspublishingqueue.go new file mode 100644 index 00000000..04d50fce --- /dev/null +++ b/indexing-service/pkg/aws/sqspublishingqueue.go @@ -0,0 +1,77 @@ +package aws + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "slices" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/fil-forge/forge/indexing-service/pkg/awsutils" + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + "github.com/fil-forge/go-ipni-tools/pkg/queue" + "github.com/ipni/go-libipni/find/model" + mh "github.com/multiformats/go-multihash" +) + +// SQSPublishingQueue implements the a queue for publishing jobs on SQS +type SQSPublishingQueue = awsutils.SQSExtendedQueue[queue.PublishingJob, model.ProviderResult] + +type jobMarshaller struct{} + +func (jm jobMarshaller) Marshall(job queue.PublishingJob) (awsutils.SerializedJob[model.ProviderResult], error) { + digests := slices.Collect(job.Digests) + data, err := json.Marshal(digests) + if err != nil { + return awsutils.SerializedJob[model.ProviderResult]{}, fmt.Errorf("serializing digests to json: %w", err) + } + reader := bytes.NewReader(data) + metaBytes, err := job.Meta.MarshalBinary() + if err != nil { + return awsutils.SerializedJob[model.ProviderResult]{}, fmt.Errorf("serializing metadata to binary: %w", err) + } + base64contextID := base64.StdEncoding.EncodeToString([]byte(job.ContextID)) + return awsutils.SerializedJob[model.ProviderResult]{ + Message: model.ProviderResult{ + Provider: &job.ProviderInfo, + ContextID: []byte(job.ContextID), + Metadata: metaBytes, + }, + Extended: reader, + GroupID: &base64contextID, + }, nil +} + +func (jm jobMarshaller) Unmarshall(sj awsutils.SerializedJob[model.ProviderResult]) (queue.PublishingJob, error) { + digests := []mh.Multihash{} + err := json.NewDecoder(sj.Extended).Decode(&digests) + if err != nil { + return queue.PublishingJob{}, fmt.Errorf("deserializing index from CAR: %w", err) + } + metadata := metadata.MetadataContext.New() + err = metadata.UnmarshalBinary(sj.Message.Metadata) + + if err != nil { + return queue.PublishingJob{}, fmt.Errorf("deserializing metadata from binary: %w", err) + } + + return queue.PublishingJob{ + ProviderInfo: *sj.Message.Provider, + ContextID: string(sj.Message.ContextID), + Meta: metadata, + Digests: slices.Values(digests), + }, nil +} + +// NewSQSPublishingQueue returns a new SQSPublishingQueue for the given aws config +func NewSQSPublishingQueue(cfg aws.Config, queueID string, bucket string) *SQSPublishingQueue { + return awsutils.NewSQSExtendedQueue(cfg, queueID, bucket, jobMarshaller{}) +} + +type SQSPublishingDecoder = awsutils.SQSDecoder[queue.PublishingJob, model.ProviderResult] + +// NewSQSPublishingDecoder returns a new SQSPublishingDecoder for the given aws config +func NewSQSPublishingDecoder(cfg aws.Config, bucket string) *SQSPublishingDecoder { + return awsutils.NewSQSDecoder(cfg, bucket, jobMarshaller{}) +} diff --git a/indexing-service/pkg/awsutils/extendedqueue.go b/indexing-service/pkg/awsutils/extendedqueue.go new file mode 100644 index 00000000..a84e577c --- /dev/null +++ b/indexing-service/pkg/awsutils/extendedqueue.go @@ -0,0 +1,203 @@ +package awsutils + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/sqs" + "github.com/fil-forge/go-ipni-tools/pkg/queue" + "github.com/google/uuid" +) + +// queueMessage is the struct that is serialized onto an SQS message queue in JSON +type queueMessage[Message any] struct { + JobID uuid.UUID `json:"JobID,omitempty"` + Message Message `json:"Message,omitempty"` +} + +// SerializedJob represents a job that has been serialized for transport over SQS + S3 +type SerializedJob[Message any] struct { + GroupID *string + Message Message + Extended io.Reader +} + +type JobMarshaller[Job any, Message any] interface { + Marshall(job Job) (SerializedJob[Message], error) + Unmarshall(SerializedJob[Message]) (Job, error) +} + +// SQSExtendedQueue implements a queue interface using SQS that can store extended data to an S3 bucket +type SQSExtendedQueue[Job any, Message any] struct { + queueID string + bucket string + s3Client *s3.Client + sqsClient *sqs.Client + decoder *SQSDecoder[Job, Message] + marshaller JobMarshaller[Job, Message] +} + +// NewSQSExtendedQueue returns a new SQSExtendedQueue for the given aws config +func NewSQSExtendedQueue[Job any, Message any](cfg aws.Config, queueID string, bucket string, marshaller JobMarshaller[Job, Message]) *SQSExtendedQueue[Job, Message] { + return &SQSExtendedQueue[Job, Message]{ + queueID: queueID, + bucket: bucket, + s3Client: s3.NewFromConfig(cfg), + sqsClient: sqs.NewFromConfig(cfg), + marshaller: marshaller, + decoder: NewSQSDecoder(cfg, bucket, marshaller), + } +} + +// Queue implements blobindexlookup.CachingQueue. +func (s *SQSExtendedQueue[Job, Message]) Queue(ctx context.Context, job Job) error { + uuid := uuid.New() + jobMessage, err := s.marshaller.Marshall(job) + if err != nil { + return fmt.Errorf("marshalling job: %w", err) + } + data, err := io.ReadAll(jobMessage.Extended) + if err != nil { + return fmt.Errorf("reading message: %w", err) + } + _, err = s.s3Client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(uuid.String()), + Body: bytes.NewReader(data), + ContentLength: aws.Int64(int64(len(data))), + }) + if err != nil { + return fmt.Errorf("saving index CAR to S3: %w", err) + } + err = s.sendMessage(ctx, jobMessage.GroupID, queueMessage[Message]{ + JobID: uuid, + Message: jobMessage.Message, + }) + if err != nil { + // error sending message so cleanup queue + _, s3deleteErr := s.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(uuid.String()), + }) + if s3deleteErr != nil { + err = errors.Join(err, fmt.Errorf("cleaning up index CAR on S3: %w", s3deleteErr)) + } + } + return err +} + +func (s *SQSExtendedQueue[Job, Message]) sendMessage(ctx context.Context, groupID *string, msg queueMessage[Message]) error { + messageJSON, err := json.Marshal(msg) + if err != nil { + return fmt.Errorf("serializing message json: %w", err) + } + message := &sqs.SendMessageInput{ + QueueUrl: aws.String(s.queueID), + MessageBody: aws.String(string(messageJSON)), + } + if groupID != nil { + message.MessageGroupId = groupID + } + _, err = s.sqsClient.SendMessage(ctx, message) + if err != nil { + return fmt.Errorf("enqueueing message: %w", err) + } + return nil +} + +// Read reads a batch of jobs from the SQS queue. +// Returns an empty slice if no jobs are available. +// The caller must process jobs and delete them from the queue when done. +func (s *SQSExtendedQueue[Job, Message]) Read(ctx context.Context, maxJobs int) ([]queue.WithID[Job], error) { + receiveOutput, err := s.sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{ + QueueUrl: aws.String(s.queueID), + MaxNumberOfMessages: int32(maxJobs), + WaitTimeSeconds: 20, // enable long-polling + }) + if err != nil { + return nil, fmt.Errorf("failed to receive messages from SQS: %w", err) + } + + if len(receiveOutput.Messages) == 0 { + return []queue.WithID[Job]{}, nil + } + + jobs := make([]queue.WithID[Job], 0, len(receiveOutput.Messages)) + for _, msg := range receiveOutput.Messages { + job, err := s.decoder.DecodeMessage(ctx, aws.ToString(msg.ReceiptHandle), aws.ToString(msg.Body)) + if err != nil { + return nil, fmt.Errorf("failed to decode message: %w", err) + } + + jobs = append(jobs, job) + } + + return jobs, nil +} + +// Release makes a job available for processing again by making it visible in the queue +func (s *SQSExtendedQueue[Job, Message]) Release(ctx context.Context, jobID string) error { + _, err := s.sqsClient.ChangeMessageVisibility(ctx, &sqs.ChangeMessageVisibilityInput{ + QueueUrl: aws.String(s.queueID), + ReceiptHandle: aws.String(jobID), + VisibilityTimeout: 0, + }) + + return err +} + +// Delete deletes a job message from the SQS queue. +func (s *SQSExtendedQueue[Job, Message]) Delete(ctx context.Context, jobID string) error { + _, err := s.sqsClient.DeleteMessage(ctx, &sqs.DeleteMessageInput{ + QueueUrl: aws.String(s.queueID), + ReceiptHandle: aws.String(jobID), + }) + return err +} + +// SQSDecoder provides interfaces for working with jobs received over SQS +type SQSDecoder[Job any, Message any] struct { + bucket string + s3Client *s3.Client + marshaller JobMarshaller[Job, Message] +} + +// NewSQSDecoder returns a new decoder for the given AWS config +func NewSQSDecoder[Job any, Message any](cfg aws.Config, bucket string, marshaller JobMarshaller[Job, Message]) *SQSDecoder[Job, Message] { + return &SQSDecoder[Job, Message]{ + bucket: bucket, + s3Client: s3.NewFromConfig(cfg), + marshaller: marshaller, + } +} + +// DecodeMessage decodes a provider caching job from the SQS message body, reading the stored index from S3 +func (s *SQSDecoder[Job, Message]) DecodeMessage(ctx context.Context, receiptHandle string, messageBody string) (queue.WithID[Job], error) { + var msg queueMessage[Message] + err := json.Unmarshal([]byte(messageBody), &msg) + if err != nil { + return queue.WithID[Job]{}, fmt.Errorf("deserializing message: %w", err) + } + received, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(msg.JobID.String()), + }) + if err != nil { + return queue.WithID[Job]{}, fmt.Errorf("reading stored index CAR: %w", err) + } + defer received.Body.Close() + job, err := s.marshaller.Unmarshall(SerializedJob[Message]{ + Message: msg.Message, + Extended: received.Body, + }) + if err != nil { + return queue.WithID[Job]{}, fmt.Errorf("unmarshalling job: %w", err) + } + return queue.WithID[Job]{ID: receiptHandle, Job: job}, nil +} diff --git a/indexing-service/pkg/build/version.go b/indexing-service/pkg/build/version.go new file mode 100644 index 00000000..f77709f2 --- /dev/null +++ b/indexing-service/pkg/build/version.go @@ -0,0 +1,68 @@ +package build + +import ( + _ "embed" + "encoding/json" + "fmt" + "os" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/revision" +) + +var ( + // version is the built version. + // Set with ldflags in .goreleaser.yaml via -ldflags="-X github.com/fil-forge/forge/indexing-service/pkg/build.version=v{{.Version}}". + version string + // Version returns the current version of the Indexing Service application + Version string + // UserAgent is the user agent used for HTTP requests + UserAgent string +) + +const ( + defaultVersion string = "v0.0.0" // Default version if not set by ldflags + versionFile string = "version.json" // Version file path +) + +func init() { + if version == "" { + // This is being ran in development, try to grab the latest known version from the version.json file + var err error + version, err = readVersionFromFile() + if err != nil { + // Use the default version + version = defaultVersion + } + } + + Version = fmt.Sprintf("%s-%s", version, revision.Revision) + UserAgent = fmt.Sprintf("indexing-service/%s", Version) +} + +// versionJson is used to read the local version.json file +type versionJSON struct { + Version string `json:"version"` +} + +// readVersionFromFile reads the version from the version.json file. +// Reading this should be fine in development since the version.json file +// should be present in the project, I hope :) +func readVersionFromFile() (string, error) { + // Open file + file, err := os.Open(versionFile) + if err != nil { + return "", err + } + defer file.Close() + + // Decode json into struct + decoder := json.NewDecoder(file) + var vJSON versionJSON + err = decoder.Decode(&vJSON) + if err != nil { + return "", err + } + + // Read version from json + return vJSON.Version, nil +} diff --git a/indexing-service/pkg/client/client.go b/indexing-service/pkg/client/client.go new file mode 100644 index 00000000..2df28fcc --- /dev/null +++ b/indexing-service/pkg/client/client.go @@ -0,0 +1,235 @@ +package client + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + + "github.com/fil-forge/forge/indexing-service/pkg/service/queryresult" + "github.com/fil-forge/forge/indexing-service/pkg/types" + assertcaps "github.com/fil-forge/forge/protocol/commands/assert" + claimcaps "github.com/fil-forge/forge/protocol/commands/claim" + "github.com/fil-forge/forge/protocol/digestutil" + "github.com/fil-forge/forge/protocol/retrieval" + "github.com/fil-forge/ucantone/client" + "github.com/fil-forge/ucantone/did" + edm "github.com/fil-forge/ucantone/errors/datamodel" + "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/fil-forge/ucantone/ucan/invocation" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const claimsPath = "/claims" + +var ErrNoReceiptFound = errors.New("missing receipt link") + +type ErrFailedResponse struct { + StatusCode int + Body string +} + +func errFromResponse(res *http.Response) ErrFailedResponse { + err := ErrFailedResponse{StatusCode: res.StatusCode} + + message, merr := io.ReadAll(res.Body) + if merr != nil { + err.Body = merr.Error() + } else { + err.Body = string(message) + } + return err +} + +func (e ErrFailedResponse) Error() string { + return fmt.Sprintf("http request failed, status: %d %s, message: %s", e.StatusCode, http.StatusText(e.StatusCode), e.Body) +} + +type Client struct { + servicePrincipal did.DID + serviceURL url.URL + client *client.HTTPClient + httpClient *http.Client + telemetryEnabled bool +} + +// execute sends the given invocation using the provided executor and decodes +// the response into the specified type. +func execute[T cbg.CBORUnmarshaler]( + ctx context.Context, + executor execution.Executor, + inv ucan.Invocation, + options ...execution.RequestOption, +) (T, ucan.Receipt, error) { + var zero T + resp, err := executor.Execute(execution.NewRequest(ctx, inv, options...)) + if err != nil { + return zero, nil, fmt.Errorf("executing invocation: %w", err) + } + + rcpt := resp.Receipt() + + o, x := rcpt.Out().Unpack() + if rcpt.Out().IsErr() { + var model edm.ErrorModel + if err := model.UnmarshalCBOR(bytes.NewReader(x)); err != nil { + return zero, nil, fmt.Errorf("executing invocation") + } + return zero, nil, fmt.Errorf("executing invocation: %w", model) + } + + // if ok is a pointer type, allocate the underlying value so + // UnmarshalCBOR has a non-nil pointer to write into. + var ok T + typ := reflect.TypeOf(ok) + if typ.Kind() == reflect.Ptr { + ok = reflect.New(typ.Elem()).Interface().(T) + } + if err := ok.UnmarshalCBOR(bytes.NewReader(o)); err != nil { + return zero, nil, fmt.Errorf("unmarshaling invocation response: %w", err) + } + return ok, rcpt, nil +} + +func (c *Client) PublishIndexClaim(ctx context.Context, issuer ucan.Issuer, args assertcaps.IndexArguments, options ...invocation.Option) error { + inv, err := assertcaps.Index.Invoke(issuer, c.servicePrincipal, &args, options...) + if err != nil { + return fmt.Errorf("generating invocation: %w", err) + } + _, _, err = execute[*assertcaps.IndexOK](ctx, c.client, inv) + return err +} + +func (c *Client) PublishEqualsClaim(ctx context.Context, issuer ucan.Issuer, args assertcaps.EqualsArguments, options ...invocation.Option) error { + inv, err := assertcaps.Equals.Invoke(issuer, c.servicePrincipal, &args, options...) + if err != nil { + return fmt.Errorf("generating invocation: %w", err) + } + _, _, err = execute[*assertcaps.EqualsOK](ctx, c.client, inv) + return err +} + +func (c *Client) CacheClaim(ctx context.Context, issuer ucan.Issuer, cacheClaim ucan.Invocation, provider claimcaps.Provider, options ...invocation.Option) error { + inv, err := claimcaps.Cache.Invoke(issuer, c.servicePrincipal, &claimcaps.CacheArguments{ + Claim: cacheClaim.Link(), + Provider: provider, + }, options...) + if err != nil { + return fmt.Errorf("generating invocation: %w", err) + } + _, _, err = execute[*claimcaps.CacheOK](ctx, c.client, inv, execution.WithInvocations(cacheClaim)) + return err +} + +func (c *Client) QueryClaims(ctx context.Context, query types.Query) (types.QueryResult, error) { + var span trace.Span + if c.telemetryEnabled { + tracer := otel.Tracer("client") + ctx, span = tracer.Start(ctx, "client.QueryClaims", + trace.WithSpanKind(trace.SpanKindClient), + ) + defer span.End() + } + + url := c.serviceURL.JoinPath(claimsPath) + q := url.Query() + q.Add("type", query.Type.String()) + for _, mh := range query.Hashes { + q.Add("multihash", digestutil.Format(mh)) + } + for _, space := range query.Match.Subject { + q.Add("spaces", space.String()) + } + url.RawQuery = q.Encode() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil) + if err != nil { + if span != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "creating request") + } + return nil, fmt.Errorf("creating request: %w", err) + } + if c.telemetryEnabled { + otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) + } + + // If there are query delegations, then add them to an X-UCAN-Container header. + if len(query.Delegations) > 0 { + msg := container.New(container.WithDelegations(query.Delegations...)) + headerValue, err := container.Encode(container.Base64urlGzip, msg) + if err != nil { + return nil, fmt.Errorf("encoding %q header: %w", retrieval.HTTPHeaderName, err) + } + req.Header.Set(retrieval.HTTPHeaderName, string(headerValue)) + } + + res, err := c.httpClient.Do(req) + if err != nil { + if span != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "sending query") + } + return nil, fmt.Errorf("sending query to server: %w", err) + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode > 299 { + if span != nil { + span.RecordError(errFromResponse(res)) + span.SetStatus(codes.Error, "non-2xx response") + } + return nil, errFromResponse(res) + } + + return queryresult.Extract(res.Body) +} + +type Option func(*Client) + +// WithHTTPClient configures the HTTP client to use for making query requests +// and invocations. +func WithHTTPClient(httpClient *http.Client) Option { + return func(c *Client) { + c.httpClient = httpClient + } +} + +// WithTelemetryEnabled toggles client-side tracing and context propagation. +func WithTelemetryEnabled(enabled bool) Option { + return func(c *Client) { + c.telemetryEnabled = enabled + } +} + +func New(servicePrincipal did.DID, serviceURL url.URL, options ...Option) (*Client, error) { + c := Client{ + servicePrincipal: servicePrincipal, + serviceURL: serviceURL, + httpClient: &http.Client{Timeout: 30 * time.Second}, + } + for _, opt := range options { + opt(&c) + } + clientOpts := []client.HTTPOption{} + if c.httpClient != nil { + clientOpts = append(clientOpts, client.WithHTTPClient(c.httpClient)) + } + client, err := client.NewHTTP(&serviceURL, clientOpts...) + if err != nil { + return nil, fmt.Errorf("creating HTTP client: %w", err) + } + c.client = client + return &c, nil +} diff --git a/indexing-service/pkg/client/client_test.go b/indexing-service/pkg/client/client_test.go new file mode 100644 index 00000000..6898d3bc --- /dev/null +++ b/indexing-service/pkg/client/client_test.go @@ -0,0 +1,294 @@ +package client + +import ( + "compress/gzip" + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/url" + "slices" + "strings" + "testing" + + "github.com/fil-forge/automobile" + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/forge/indexing-service/pkg/service/queryresult" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/bytemap" + "github.com/fil-forge/forge/protocol/commands" + assertcaps "github.com/fil-forge/forge/protocol/commands/assert" + claimcaps "github.com/fil-forge/forge/protocol/commands/claim" + "github.com/fil-forge/forge/protocol/digestutil" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + "github.com/ipni/go-libipni/maurl" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +func TestClient(t *testing.T) { + indexingID := testutil.Service + + // Track invocations seen by the indexing UCAN server. + indexer := newRecordingIndexer() + ucanSrv, err := contentclaims.NewUCANServer(indexingID, indexer) + require.NoError(t, err) + indexingUCANServer := httptest.NewServer(ucanSrv) + t.Cleanup(indexingUCANServer.Close) + indexingURL := *testutil.Must(url.Parse(indexingUCANServer.URL))(t) + + // Self-signed invocations: use the indexing service signer as the issuer + // so the server-side validator accepts them without a delegation chain. + storageID := indexingID + storageURL := indexingURL + alice := indexingID + + contentDigest := testutil.RandomMultihash(t) + indexCID := testutil.RandomCID(t) + + locationClaim, err := assertcaps.Location.Invoke( + storageID, + storageID.DID(), + &assertcaps.LocationArguments{ + Space: alice.DID(), + Content: contentDigest, + Location: []commands.CborURL{commands.CborURL(*testutil.TestURL)}, + }, + ) + require.NoError(t, err) + + provider := claimcaps.Provider{ + Addresses: [][]byte{ + testutil.Must(maurl.FromURL(storageURL.JoinPath("/claim/{claim}")))(t).Bytes(), + testutil.Must(maurl.FromURL(storageURL.JoinPath("/blob/{blob}")))(t).Bytes(), + }, + } + + t.Run("cache claim", func(t *testing.T) { + indexer.reset() + c, err := New(indexingID.DID(), indexingURL) + require.NoError(t, err) + + err = c.CacheClaim(context.Background(), storageID, locationClaim, provider) + require.NoError(t, err) + + require.NotEmpty(t, indexer.cached) + require.Equal(t, locationClaim.Link(), indexer.cached[len(indexer.cached)-1].Link()) + }) + + t.Run("publish index claim", func(t *testing.T) { + indexer.reset() + c, err := New(indexingID.DID(), indexingURL) + require.NoError(t, err) + + err = c.PublishIndexClaim( + context.Background(), + alice, + assertcaps.IndexArguments{Index: indexCID}, + ) + require.NoError(t, err) + + require.NotEmpty(t, indexer.published) + require.Equal(t, assertcaps.Index.Command, indexer.published[len(indexer.published)-1].Command()) + }) + + t.Run("query claims", func(t *testing.T) { + for _, tc := range []struct { + name string + detectGzip bool + }{ + {name: "without gzip", detectGzip: false}, + {name: "with gzip", detectGzip: true}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Run("basic", func(t *testing.T) { + results := bytemap.NewByteMap[multihash.Multihash, types.QueryResult](-1) + srv := mockQueryServer(t, results, config{detectGzip: tc.detectGzip}) + t.Cleanup(srv.Close) + + c, err := New(indexingID.DID(), *testutil.Must(url.Parse(srv.URL))(t)) + require.NoError(t, err) + + claims := map[cid.Cid]ucan.Invocation{ + locationClaim.Link(): locationClaim, + } + indexes := bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](-1) + results.Set(contentDigest, testutil.Must(queryresult.Build(claims, indexes))(t)) + + res, err := c.QueryClaims(context.Background(), types.Query{ + Hashes: []multihash.Multihash{contentDigest}, + }) + require.NoError(t, err) + + require.True(t, slices.ContainsFunc(res.Claims(), func(c cid.Cid) bool { + return c == locationClaim.Link() + })) + }) + + t.Run("query requests the right type", func(t *testing.T) { + results := bytemap.NewByteMap[multihash.Multihash, types.QueryResult](-1) + srv := mockQueryServer(t, results, config{detectGzip: tc.detectGzip}) + t.Cleanup(srv.Close) + + c, err := New(indexingID.DID(), *testutil.Must(url.Parse(srv.URL))(t)) + require.NoError(t, err) + + claims := map[cid.Cid]ucan.Invocation{} + indexes := bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](-1) + results.Set(contentDigest, testutil.Must(queryresult.Build(claims, indexes))(t)) + + t.Run("standard", func(t *testing.T) { + _, err := c.QueryClaims(context.Background(), types.Query{Hashes: []multihash.Multihash{contentDigest}}) + require.NoError(t, err) + require.Contains(t, srv.lastRequestedURL, "type=standard") + }) + + t.Run("location", func(t *testing.T) { + _, err := c.QueryClaims(context.Background(), types.Query{ + Type: types.QueryTypeLocation, + Hashes: []multihash.Multihash{contentDigest}, + }) + require.NoError(t, err) + require.Contains(t, srv.lastRequestedURL, "type=location") + }) + + t.Run("index_or_location", func(t *testing.T) { + _, err := c.QueryClaims(context.Background(), types.Query{ + Type: types.QueryTypeIndexOrLocation, + Hashes: []multihash.Multihash{contentDigest}, + }) + require.NoError(t, err) + require.Contains(t, srv.lastRequestedURL, "type=index_or_location") + }) + }) + + t.Run("query throws error", func(t *testing.T) { + results := bytemap.NewByteMap[multihash.Multihash, types.QueryResult](-1) + srv := mockQueryServer(t, results, config{detectGzip: tc.detectGzip, throwError: errors.New("something went terribly wrong")}) + t.Cleanup(srv.Close) + + c, err := New(indexingID.DID(), *testutil.Must(url.Parse(srv.URL))(t)) + require.NoError(t, err) + + _, err = c.QueryClaims(context.Background(), types.Query{ + Hashes: []multihash.Multihash{contentDigest}, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "something went terribly wrong") + }) + }) + } + }) +} + +// recordingIndexer captures invocations and acts as a UCAN service. +type recordingIndexer struct { + cached []ucan.Invocation + published []ucan.Invocation +} + +func newRecordingIndexer() *recordingIndexer { return &recordingIndexer{} } + +func (r *recordingIndexer) reset() { + r.cached = nil + r.published = nil +} + +func (r *recordingIndexer) Cache(ctx context.Context, provider peer.AddrInfo, claim ucan.Invocation, meta ucan.Container) error { + r.cached = append(r.cached, claim) + return nil +} + +func (r *recordingIndexer) Publish(ctx context.Context, claim ucan.Invocation, meta ucan.Container) error { + r.published = append(r.published, claim) + return nil +} + +type mockServer struct { + *httptest.Server + lastRequestedURL string + lastRequestedHeader http.Header +} + +type config struct { + throwError error + detectGzip bool +} + +func mockQueryServer(_ *testing.T, results bytemap.ByteMap[multihash.Multihash, types.QueryResult], cfg config) *mockServer { + ms := &mockServer{} + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ms.lastRequestedURL = r.URL.String() + ms.lastRequestedHeader = r.Header + + mhStrings := r.URL.Query()["multihash"] + if len(mhStrings) != 1 { + http.Error(w, "mock query service supports only single hash", http.StatusNotImplemented) + return + } + + digest, err := digestutil.Parse(mhStrings[0]) + if err != nil { + http.Error(w, "invalid digest", http.StatusBadRequest) + return + } + + if cfg.throwError != nil { + http.Error(w, cfg.throwError.Error(), http.StatusInternalServerError) + return + } + + qr := results.Get(digest) + if qr == nil { + qr, _ = queryresult.Build( + map[cid.Cid]ucan.Invocation{}, + bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](-1), + ) + } + + blks := make([]automobile.Block, 0, len(qr.Blocks())) + for _, blk := range qr.Blocks() { + blks = append(blks, automobile.Block{Link: blk.Link, Data: blk.Data}) + } + w.WriteHeader(http.StatusOK) + body := automobile.Encode([]cid.Cid{qr.Root().Link}, blks) + _, _ = io.Copy(w, body) + }) + + if cfg.detectGzip { + handler = withGzip(handler) + } + + ms.Server = httptest.NewServer(handler) + return ms +} + +type gzipResponseWriter struct { + io.Writer + http.ResponseWriter +} + +func (w gzipResponseWriter) Write(b []byte) (int, error) { return w.Writer.Write(b) } + +func withGzip(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + handler(w, r) + return + } + w.Header().Set("Content-Encoding", "gzip") + gz := gzip.NewWriter(w) + defer gz.Close() + handler(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r) + } +} + +// silence unused imports if some are not needed +var _ = multiaddr.NewMultiaddr diff --git a/indexing-service/pkg/construct/construct.go b/indexing-service/pkg/construct/construct.go new file mode 100644 index 00000000..9338a650 --- /dev/null +++ b/indexing-service/pkg/construct/construct.go @@ -0,0 +1,508 @@ +package construct + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/fil-forge/forge/indexing-service/pkg/service" + "github.com/fil-forge/forge/indexing-service/pkg/service/blobindexlookup" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/forge/indexing-service/pkg/service/providercacher" + "github.com/fil-forge/forge/indexing-service/pkg/service/providerindex" + "github.com/fil-forge/forge/indexing-service/pkg/service/providerindex/remotesyncer" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/internal/jobqueue" + ipniclient "github.com/fil-forge/go-ipni-tools/pkg/client" + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + "github.com/fil-forge/go-ipni-tools/pkg/notifier" + "github.com/fil-forge/go-ipni-tools/pkg/publisher" + "github.com/fil-forge/go-ipni-tools/pkg/server" + "github.com/fil-forge/go-ipni-tools/pkg/store" + "github.com/fil-forge/ucantone/multikey" + "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/namespace" + dssync "github.com/ipfs/go-datastore/sync" + flatfs "github.com/ipfs/go-ds-flatfs" + logging "github.com/ipfs/go-log/v2" + ipnifind "github.com/ipni/go-libipni/find/client" + "github.com/ipni/go-libipni/maurl" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + goredis "github.com/redis/go-redis/v9" +) + +const IPNIFindTimeout = 2500 * time.Millisecond + +var log = logging.Logger("service") +var providerIndexNamespace = datastore.NewKey("providerindex/") +var providerIndexPublisherNamespace = providerIndexNamespace.Child(datastore.NewKey("publisher/")) +var contentClaimsNamespace = datastore.NewKey("claims/") + +// ServiceConfig sets specific config values for the service +type ServiceConfig struct { + // ID configures the identity for the service. Both ID and PrivateKey use the + // same key, the difference is that ID may be a did:web. + ID multikey.Issuer + + // PrivateKey configures the private key for the service. + PrivateKey crypto.PrivKey + + // PublicURL is the public HTTP URL(s) the indexing service is available at. + // These are used when publishing claims, to indicate that they can be + // retrieved from here, replacing the pattern "{claim}" with the CID of the + // claim that is available. + PublicURL []string + + ProvidersRedis goredis.ClusterOptions + NoProviderRedis goredis.ClusterOptions + ClaimsRedis goredis.ClusterOptions + IndexesRedis goredis.ClusterOptions + + // IPNIFindURL is the URL of an IPNI node to use for find queries. + IPNIFindURL string + // IPNIFindFallbackURLs are the URL(s) of IPNI nodes to use for find queries + // if the main URL is unavailable or returning no results. + IPNIFindFallbackURLs []string + + // IPNIDirectAnnounceURLs are the URL(s) of IPNI nodes that + // advertisement announcements should be sent to. Defaults to IndexerURL if + // not set. + IPNIDirectAnnounceURLs []string + // IPNIListenAddr configures the HTTP address the publisher binds to. + // This allows a remote IPNI subscriber to fetch advertisements. + IPNIListenAddr string + // IPNIAnnounceAddrs configures the multiaddrs that are put into announce + // messages to tell indexers the addresses to fetch advertisements from. + IPNIAnnounceAddrs []string +} + +type config struct { + cachingQueue providercacher.CachingQueueQueuer + opts []service.Option + ds datastore.Batching + skipNotification bool + startIPNIServer bool + publisherStore store.PublisherStore + asyncPublisher publisher.AsyncPublisher + claimsStore types.ContentClaimsStore + providersClient redis.PipelineClient + noProvidersClient redis.Client + claimsClient redis.Client + indexesClient redis.Client + providersCacheOpts []redis.Option + noProvidersCacheOpts []redis.Option + claimsCacheOpts []redis.Option + indexesCacheOpts []redis.Option + + httpClient *http.Client + provIndexLog logging.EventLogger +} + +// DefaultHTTPClient creates a HTTP client with sensible defaults +func DefaultHTTPClient() *http.Client { + return &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 5 * time.Second, + }).Dial, + TLSHandshakeTimeout: 5 * time.Second, + }, + } +} + +// Option configures how the node is construct +type Option func(*config) error + +// WithServiceOptions passes option to the core service +func WithServiceOptions(opts ...service.Option) Option { + return func(cfg *config) error { + cfg.opts = opts + return nil + } +} + +// WithCachingQueue overrides the default caching queue for provider caching +func WithCachingQueue(cachingQueue providercacher.CachingQueueQueuer) Option { + return func(cfg *config) error { + cfg.cachingQueue = cachingQueue + return nil + } +} + +// SkipNotification removes setting up IPNI notification handlers +func SkipNotification() Option { + return func(cfg *config) error { + cfg.skipNotification = true + return nil + } +} + +// WithStartIPNIServer determines when IPNI ads will be served directly over HTTP +// Defaults true +func WithStartIPNIServer(startIPNIServer bool) Option { + return func(cfg *config) error { + cfg.startIPNIServer = startIPNIServer + return nil + } +} + +// WithAsyncPublisher overrides the default IPNI publisher +func WithAsyncPublisher(asyncPublisher publisher.AsyncPublisher) Option { + return func(cfg *config) error { + cfg.asyncPublisher = asyncPublisher + return nil + } +} + +// WithPublisherStore overrides the store used for IPNI advertisements +// If not used with startIPNIServer = false, store.AdvertStore must implement store.FullStore +func WithPublisherStore(publisherStore store.PublisherStore) Option { + return func(cfg *config) error { + cfg.publisherStore = publisherStore + return nil + } +} + +// WithClaimsStore configures the store used for content claims. +func WithClaimsStore(store types.ContentClaimsStore) Option { + return func(cfg *config) error { + cfg.claimsStore = store + return nil + } +} + +// WithDataPath constructs a flat FS datastore at the specified path to use for +// IPNI advertisements and content claims. +func WithDataPath(dataPath string) Option { + return func(cfg *config) error { + fds, err := flatfs.CreateOrOpen(dataPath, flatfs.IPFS_DEF_SHARD, true) + if err != nil { + return fmt.Errorf("creating or opening IPNI publisher datastore: %w", err) + } + cfg.ds = fds + return nil + } +} + +// WithDatastore uses the given datastore for storing IPNI advertisements and +// content claims. +func WithDatastore(ds datastore.Batching) Option { + return func(cfg *config) error { + cfg.ds = ds + return nil + } +} + +// WithProvidersClient configures the redis client used for caching providers. +func WithProvidersClient(client redis.PipelineClient) Option { + return func(cfg *config) error { + cfg.providersClient = client + return nil + } +} + +// WithNoProvidersClient configures the redis client used for caching empty provider results +func WithNoProvidersClient(client redis.Client) Option { + return func(cfg *config) error { + cfg.noProvidersClient = client + return nil + } +} + +// WithClaimsClient configures the redis client used for caching content claims. +func WithClaimsClient(client redis.Client) Option { + return func(cfg *config) error { + cfg.claimsClient = client + return nil + } +} + +// WithIndexesClient configures the redis client used for caching blob indexes. +func WithIndexesClient(client redis.Client) Option { + return func(cfg *config) error { + cfg.indexesClient = client + return nil + } +} + +// WithHTTPClient configures the HTTP client used when consuming HTTP APIs +func WithHTTPClient(httpClient *http.Client) Option { + return func(cfg *config) error { + cfg.httpClient = httpClient + return nil + } +} + +// WithProvidersCacheOptions passes configuration to the providers cache +func WithProvidersCacheOptions(opts ...redis.Option) Option { + return func(cfg *config) error { + cfg.providersCacheOpts = opts + return nil + } +} + +// WithNoProvidersCacheOptions passes configuration to the no providers cache +func WithNoProvidersCacheOptions(opts ...redis.Option) Option { + return func(cfg *config) error { + cfg.noProvidersCacheOpts = opts + return nil + } +} + +// WithClaimsCacheOptions passes configuration to the providers cache +func WithClaimsCacheOptions(opts ...redis.Option) Option { + return func(cfg *config) error { + cfg.claimsCacheOpts = opts + return nil + } +} + +// WithIndexesCacheOptions passes configuration to the providers cache +func WithIndexesCacheOptions(opts ...redis.Option) Option { + return func(cfg *config) error { + cfg.indexesCacheOpts = opts + return nil + } +} + +func WithProviderIndexLogger(log logging.EventLogger) Option { + return func(cfg *config) error { + cfg.provIndexLog = log + return nil + } +} + +// Service is the core methods of the indexing service but with additional +// lifecycle methods. +type Service interface { + types.Service + Startup(ctx context.Context) error + Shutdown(ctx context.Context) error +} + +type serviceWithLifeCycle struct { + *service.IndexingService + startupFuncs []func(ctx context.Context) error + shutdownFuncs []func(ctx context.Context) error +} + +func (s *serviceWithLifeCycle) Startup(ctx context.Context) error { + for _, startupFunc := range s.startupFuncs { + err := startupFunc(ctx) + if err != nil { + return err + } + } + return nil +} + +func (s *serviceWithLifeCycle) Shutdown(ctx context.Context) error { + for _, shutdownFunc := range s.shutdownFuncs { + err := shutdownFunc(ctx) + if err != nil { + return err + } + } + return nil +} + +// Construct constructs a full operational indexing service, using real dependencies +func Construct(sc ServiceConfig, opts ...Option) (Service, error) { + var cfg config + for _, opt := range opts { + err := opt(&cfg) + if err != nil { + return nil, err + } + } + + s := &serviceWithLifeCycle{} + // connect to redis + providersClient := cfg.providersClient + if providersClient == nil { + providersClient = redis.NewClusterClientAdapter(goredis.NewClusterClient(&sc.ProvidersRedis)) + } + noProvidersClient := cfg.noProvidersClient + if noProvidersClient == nil { + noProvidersClient = goredis.NewClusterClient(&sc.NoProviderRedis) + } + claimsClient := cfg.claimsClient + if claimsClient == nil { + claimsClient = goredis.NewClusterClient(&sc.ClaimsRedis) + } + indexesClient := cfg.indexesClient + if indexesClient == nil { + indexesClient = goredis.NewClusterClient(&sc.IndexesRedis) + } + + // build caches + providersCache := redis.NewProviderStore(providersClient, cfg.providersCacheOpts...) + noProvidersCache := redis.NewNoProviderStore(noProvidersClient, cfg.noProvidersCacheOpts...) + claimsCache := redis.NewContentClaimsStore(claimsClient, cfg.claimsCacheOpts...) + shardDagIndexesCache := redis.NewShardedDagIndexStore(indexesClient, cfg.indexesCacheOpts...) + + cachingQueue := cfg.cachingQueue + if cachingQueue == nil { + // setup and start the provider caching queue for indexes + cachingJobHandler := providercacher.NewJobHandler(providercacher.NewSimpleProviderCacher(providersCache)) + + jq := jobqueue.NewJobQueue[providercacher.ProviderCachingJob]( + jobqueue.JobHandler(cachingJobHandler.Handle), + jobqueue.WithBuffer(5), + jobqueue.WithConcurrency(5), + jobqueue.WithErrorHandler(func(err error) { + log.Errorw("caching provider index", "error", err) + })) + + s.startupFuncs = append(s.startupFuncs, func(context.Context) error { jq.Startup(); return nil }) + s.shutdownFuncs = append(s.shutdownFuncs, jq.Shutdown) + cachingQueue = jq + } + + httpClient := DefaultHTTPClient() + if cfg.httpClient != nil { + httpClient = cfg.httpClient + } + + // setup IPNI + // TODO: switch to double hashed client for reader privacy? + var findClient ipnifind.Finder + findClient, err := ipnifind.New(sc.IPNIFindURL, ipnifind.WithClient(httpClient)) + if err != nil { + return nil, err + } + if len(sc.IPNIFindFallbackURLs) > 0 { + tiers := []ipnifind.Finder{findClient} + for _, url := range sc.IPNIFindFallbackURLs { + finder, err := ipnifind.New(url, ipnifind.WithClient(httpClient)) + if err != nil { + return nil, fmt.Errorf("creating IPNI fallback find client for %s: %w", url, err) + } + tiers = append(tiers, finder) + } + findClient, err = ipniclient.NewTieredFinder(tiers, ipniclient.WithTierFindTimeout(IPNIFindTimeout)) + if err != nil { + return nil, fmt.Errorf("creating tiered IPNI find client: %w", err) + } + } + + var ds datastore.Batching + publisherStore := cfg.publisherStore + if publisherStore == nil { + ds = initializeDatastore(&cfg) + // setup the datastore for publishing to IPNI + publisherStore = store.FromDatastore( + namespace.Wrap(ds, providerIndexPublisherNamespace), + store.WithMetadataContext(metadata.MetadataContext), + ) + } + + // setup remote sync notification + if !cfg.skipNotification { + // initialize datastore if not already initialized + if ds == nil { + ds = initializeDatastore(&cfg) + } + notifierStore := store.SimpleStoreFromDatastore(namespace.Wrap(ds, providerIndexNamespace)) + notifier, err := notifier.NewNotifierWithStorage(sc.IPNIFindURL, sc.PrivateKey, notifierStore) + if err != nil { + return nil, fmt.Errorf("creating IPNI remote sync notifier: %w", err) + } + s.startupFuncs = append(s.startupFuncs, func(ctx context.Context) error { notifier.Start(ctx); return nil }) + s.shutdownFuncs = append(s.shutdownFuncs, func(context.Context) error { notifier.Stop(); return nil }) + // Setup handling ipni remote sync notifications + notifier.Notify(remotesyncer.New(providersCache, publisherStore).HandleRemoteSync) + } + + asyncPublisher := cfg.asyncPublisher + if asyncPublisher == nil { + + directAnnounceURLs := sc.IPNIDirectAnnounceURLs + if len(directAnnounceURLs) == 0 { + directAnnounceURLs = append(directAnnounceURLs, sc.IPNIFindURL) + } + + ipniPublisher, err := publisher.New( + sc.PrivateKey, + publisherStore, + publisher.WithDirectAnnounce(directAnnounceURLs...), + publisher.WithAnnounceAddrs(sc.IPNIAnnounceAddrs...), + ) + if err != nil { + return nil, fmt.Errorf("creating IPNI publisher: %w", err) + } + asyncPublisher = publisher.AsyncFrom(ipniPublisher) + } + + if cfg.startIPNIServer { + encodableStore, ok := publisherStore.(store.EncodeableStore) + if !ok { + return nil, fmt.Errorf("publisher store is incompatible with serving over HTTP (must implement store.EncodableStore)") + } + srv, err := server.NewServer(encodableStore, server.WithHTTPListenAddrs(sc.IPNIListenAddr)) + if err != nil { + return nil, fmt.Errorf("creating server for IPNI ads: %w", err) + } + s.startupFuncs = append(s.startupFuncs, srv.Start) + s.shutdownFuncs = append(s.shutdownFuncs, srv.Shutdown) + } + + providerIndex := providerindex.New(providersCache, noProvidersCache, findClient, asyncPublisher, providerindex.WithLogger(cfg.provIndexLog)) + + claimsStore := cfg.claimsStore + if claimsStore == nil { + if ds == nil { + ds = initializeDatastore(&cfg) + } + claimsStore = contentclaims.NewStoreFromDatastore(namespace.Wrap(ds, contentClaimsNamespace)) + } + + finder := contentclaims.NewSimpleFinder(httpClient) + + claims := contentclaims.New(claimsStore, claimsCache, finder) + blobIndexLookup := blobindexlookup.WithCache( + blobindexlookup.NewBlobIndexLookup(httpClient), + shardDagIndexesCache, + cachingQueue, + ) + + peerID, err := peer.IDFromPrivateKey(sc.PrivateKey) + if err != nil { + return nil, fmt.Errorf("creating peer ID: %w", err) + } + + publicAddrInfo := peer.AddrInfo{ID: peerID} + for _, str := range sc.PublicURL { + u, err := url.Parse(str) + if err != nil { + return nil, fmt.Errorf("parsing public URL: %w", err) + } + addr, err := maurl.FromURL(u) + if err != nil { + return nil, fmt.Errorf("converting URL to multiaddr: %w", err) + } + publicAddrInfo.Addrs = append(publicAddrInfo.Addrs, addr) + } + + // with concurrency will still get overridden if a different walker setting is used + serviceOpts := append([]service.Option{service.WithConcurrency(15)}, cfg.opts...) + + s.IndexingService = service.NewIndexingService(sc.ID, blobIndexLookup, claims, publicAddrInfo, providerIndex, serviceOpts...) + + return s, nil +} + +func initializeDatastore(cfg *config) datastore.Batching { + ds := cfg.ds + if ds == nil { + log.Warnf("datastore not configured, using in-memory store") + ds = dssync.MutexWrap(datastore.NewMapDatastore()) + } + return ds +} diff --git a/indexing-service/pkg/internal/extmocks/helpers.go b/indexing-service/pkg/internal/extmocks/helpers.go new file mode 100644 index 00000000..da692091 --- /dev/null +++ b/indexing-service/pkg/internal/extmocks/helpers.go @@ -0,0 +1,15 @@ +package extmocks + +import ( + context "context" + + "github.com/stretchr/testify/mock" +) + +// AnyContext is a matcher for testify/mock that matches anything implementing context.Context +var AnyContext = mock.MatchedBy(func(c context.Context) bool { + // if the passed in parameter does not implement the context.Context interface, the + // wrapping MatchedBy will panic - so we can simply return true, since we + // know it's a context.Context if execution flow makes it here. + return true +}) diff --git a/indexing-service/pkg/internal/extmocks/mock_DynamoDBQueryClient.go b/indexing-service/pkg/internal/extmocks/mock_DynamoDBQueryClient.go new file mode 100644 index 00000000..1cf102ac --- /dev/null +++ b/indexing-service/pkg/internal/extmocks/mock_DynamoDBQueryClient.go @@ -0,0 +1,126 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package extmocks + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + mock "github.com/stretchr/testify/mock" +) + +// NewMockDynamoDBQueryClient creates a new instance of MockDynamoDBQueryClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockDynamoDBQueryClient(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDynamoDBQueryClient { + mock := &MockDynamoDBQueryClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockDynamoDBQueryClient is an autogenerated mock type for the QueryAPIClient type +type MockDynamoDBQueryClient struct { + mock.Mock +} + +type MockDynamoDBQueryClient_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDynamoDBQueryClient) EXPECT() *MockDynamoDBQueryClient_Expecter { + return &MockDynamoDBQueryClient_Expecter{mock: &_m.Mock} +} + +// Query provides a mock function for the type MockDynamoDBQueryClient +func (_mock *MockDynamoDBQueryClient) Query(context1 context.Context, queryInput *dynamodb.QueryInput, fns ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) { + // func(*dynamodb.Options) + _va := make([]interface{}, len(fns)) + for _i := range fns { + _va[_i] = fns[_i] + } + var _ca []interface{} + _ca = append(_ca, context1, queryInput) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Query") + } + + var r0 *dynamodb.QueryOutput + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *dynamodb.QueryInput, ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error)); ok { + return returnFunc(context1, queryInput, fns...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, *dynamodb.QueryInput, ...func(*dynamodb.Options)) *dynamodb.QueryOutput); ok { + r0 = returnFunc(context1, queryInput, fns...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dynamodb.QueryOutput) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, *dynamodb.QueryInput, ...func(*dynamodb.Options)) error); ok { + r1 = returnFunc(context1, queryInput, fns...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockDynamoDBQueryClient_Query_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Query' +type MockDynamoDBQueryClient_Query_Call struct { + *mock.Call +} + +// Query is a helper method to define mock.On call +// - context1 context.Context +// - queryInput *dynamodb.QueryInput +// - fns ...func(*dynamodb.Options) +func (_e *MockDynamoDBQueryClient_Expecter) Query(context1 interface{}, queryInput interface{}, fns ...interface{}) *MockDynamoDBQueryClient_Query_Call { + return &MockDynamoDBQueryClient_Query_Call{Call: _e.mock.On("Query", + append([]interface{}{context1, queryInput}, fns...)...)} +} + +func (_c *MockDynamoDBQueryClient_Query_Call) Run(run func(context1 context.Context, queryInput *dynamodb.QueryInput, fns ...func(*dynamodb.Options))) *MockDynamoDBQueryClient_Query_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *dynamodb.QueryInput + if args[1] != nil { + arg1 = args[1].(*dynamodb.QueryInput) + } + var arg2 []func(*dynamodb.Options) + variadicArgs := make([]func(*dynamodb.Options), len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(func(*dynamodb.Options)) + } + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockDynamoDBQueryClient_Query_Call) Return(queryOutput *dynamodb.QueryOutput, err error) *MockDynamoDBQueryClient_Query_Call { + _c.Call.Return(queryOutput, err) + return _c +} + +func (_c *MockDynamoDBQueryClient_Query_Call) RunAndReturn(run func(context1 context.Context, queryInput *dynamodb.QueryInput, fns ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error)) *MockDynamoDBQueryClient_Query_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/internal/extmocks/mock_EventLogger.go b/indexing-service/pkg/internal/extmocks/mock_EventLogger.go new file mode 100644 index 00000000..42c360b7 --- /dev/null +++ b/indexing-service/pkg/internal/extmocks/mock_EventLogger.go @@ -0,0 +1,642 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package extmocks + +import ( + mock "github.com/stretchr/testify/mock" +) + +// NewMockEventLogger creates a new instance of MockEventLogger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockEventLogger(t interface { + mock.TestingT + Cleanup(func()) +}) *MockEventLogger { + mock := &MockEventLogger{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockEventLogger is an autogenerated mock type for the EventLogger type +type MockEventLogger struct { + mock.Mock +} + +type MockEventLogger_Expecter struct { + mock *mock.Mock +} + +func (_m *MockEventLogger) EXPECT() *MockEventLogger_Expecter { + return &MockEventLogger_Expecter{mock: &_m.Mock} +} + +// Debug provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Debug(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Debug_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Debug' +type MockEventLogger_Debug_Call struct { + *mock.Call +} + +// Debug is a helper method to define mock.On call +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Debug(args ...interface{}) *MockEventLogger_Debug_Call { + return &MockEventLogger_Debug_Call{Call: _e.mock.On("Debug", + append([]interface{}{}, args...)...)} +} + +func (_c *MockEventLogger_Debug_Call) Run(run func(args ...interface{})) *MockEventLogger_Debug_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []interface{} + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg0 = variadicArgs + run( + arg0..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Debug_Call) Return() *MockEventLogger_Debug_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Debug_Call) RunAndReturn(run func(args ...interface{})) *MockEventLogger_Debug_Call { + _c.Run(run) + return _c +} + +// Debugf provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Debugf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Debugf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Debugf' +type MockEventLogger_Debugf_Call struct { + *mock.Call +} + +// Debugf is a helper method to define mock.On call +// - format string +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Debugf(format interface{}, args ...interface{}) *MockEventLogger_Debugf_Call { + return &MockEventLogger_Debugf_Call{Call: _e.mock.On("Debugf", + append([]interface{}{format}, args...)...)} +} + +func (_c *MockEventLogger_Debugf_Call) Run(run func(format string, args ...interface{})) *MockEventLogger_Debugf_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 []interface{} + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Debugf_Call) Return() *MockEventLogger_Debugf_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Debugf_Call) RunAndReturn(run func(format string, args ...interface{})) *MockEventLogger_Debugf_Call { + _c.Run(run) + return _c +} + +// Error provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Error(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error' +type MockEventLogger_Error_Call struct { + *mock.Call +} + +// Error is a helper method to define mock.On call +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Error(args ...interface{}) *MockEventLogger_Error_Call { + return &MockEventLogger_Error_Call{Call: _e.mock.On("Error", + append([]interface{}{}, args...)...)} +} + +func (_c *MockEventLogger_Error_Call) Run(run func(args ...interface{})) *MockEventLogger_Error_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []interface{} + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg0 = variadicArgs + run( + arg0..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Error_Call) Return() *MockEventLogger_Error_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Error_Call) RunAndReturn(run func(args ...interface{})) *MockEventLogger_Error_Call { + _c.Run(run) + return _c +} + +// Errorf provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Errorf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Errorf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Errorf' +type MockEventLogger_Errorf_Call struct { + *mock.Call +} + +// Errorf is a helper method to define mock.On call +// - format string +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Errorf(format interface{}, args ...interface{}) *MockEventLogger_Errorf_Call { + return &MockEventLogger_Errorf_Call{Call: _e.mock.On("Errorf", + append([]interface{}{format}, args...)...)} +} + +func (_c *MockEventLogger_Errorf_Call) Run(run func(format string, args ...interface{})) *MockEventLogger_Errorf_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 []interface{} + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Errorf_Call) Return() *MockEventLogger_Errorf_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Errorf_Call) RunAndReturn(run func(format string, args ...interface{})) *MockEventLogger_Errorf_Call { + _c.Run(run) + return _c +} + +// Fatal provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Fatal(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Fatal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fatal' +type MockEventLogger_Fatal_Call struct { + *mock.Call +} + +// Fatal is a helper method to define mock.On call +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Fatal(args ...interface{}) *MockEventLogger_Fatal_Call { + return &MockEventLogger_Fatal_Call{Call: _e.mock.On("Fatal", + append([]interface{}{}, args...)...)} +} + +func (_c *MockEventLogger_Fatal_Call) Run(run func(args ...interface{})) *MockEventLogger_Fatal_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []interface{} + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg0 = variadicArgs + run( + arg0..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Fatal_Call) Return() *MockEventLogger_Fatal_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Fatal_Call) RunAndReturn(run func(args ...interface{})) *MockEventLogger_Fatal_Call { + _c.Run(run) + return _c +} + +// Fatalf provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Fatalf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Fatalf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fatalf' +type MockEventLogger_Fatalf_Call struct { + *mock.Call +} + +// Fatalf is a helper method to define mock.On call +// - format string +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Fatalf(format interface{}, args ...interface{}) *MockEventLogger_Fatalf_Call { + return &MockEventLogger_Fatalf_Call{Call: _e.mock.On("Fatalf", + append([]interface{}{format}, args...)...)} +} + +func (_c *MockEventLogger_Fatalf_Call) Run(run func(format string, args ...interface{})) *MockEventLogger_Fatalf_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 []interface{} + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Fatalf_Call) Return() *MockEventLogger_Fatalf_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Fatalf_Call) RunAndReturn(run func(format string, args ...interface{})) *MockEventLogger_Fatalf_Call { + _c.Run(run) + return _c +} + +// Info provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Info(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Info_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Info' +type MockEventLogger_Info_Call struct { + *mock.Call +} + +// Info is a helper method to define mock.On call +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Info(args ...interface{}) *MockEventLogger_Info_Call { + return &MockEventLogger_Info_Call{Call: _e.mock.On("Info", + append([]interface{}{}, args...)...)} +} + +func (_c *MockEventLogger_Info_Call) Run(run func(args ...interface{})) *MockEventLogger_Info_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []interface{} + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg0 = variadicArgs + run( + arg0..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Info_Call) Return() *MockEventLogger_Info_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Info_Call) RunAndReturn(run func(args ...interface{})) *MockEventLogger_Info_Call { + _c.Run(run) + return _c +} + +// Infof provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Infof(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Infof_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Infof' +type MockEventLogger_Infof_Call struct { + *mock.Call +} + +// Infof is a helper method to define mock.On call +// - format string +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Infof(format interface{}, args ...interface{}) *MockEventLogger_Infof_Call { + return &MockEventLogger_Infof_Call{Call: _e.mock.On("Infof", + append([]interface{}{format}, args...)...)} +} + +func (_c *MockEventLogger_Infof_Call) Run(run func(format string, args ...interface{})) *MockEventLogger_Infof_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 []interface{} + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Infof_Call) Return() *MockEventLogger_Infof_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Infof_Call) RunAndReturn(run func(format string, args ...interface{})) *MockEventLogger_Infof_Call { + _c.Run(run) + return _c +} + +// Panic provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Panic(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Panic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Panic' +type MockEventLogger_Panic_Call struct { + *mock.Call +} + +// Panic is a helper method to define mock.On call +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Panic(args ...interface{}) *MockEventLogger_Panic_Call { + return &MockEventLogger_Panic_Call{Call: _e.mock.On("Panic", + append([]interface{}{}, args...)...)} +} + +func (_c *MockEventLogger_Panic_Call) Run(run func(args ...interface{})) *MockEventLogger_Panic_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []interface{} + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg0 = variadicArgs + run( + arg0..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Panic_Call) Return() *MockEventLogger_Panic_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Panic_Call) RunAndReturn(run func(args ...interface{})) *MockEventLogger_Panic_Call { + _c.Run(run) + return _c +} + +// Panicf provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Panicf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Panicf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Panicf' +type MockEventLogger_Panicf_Call struct { + *mock.Call +} + +// Panicf is a helper method to define mock.On call +// - format string +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Panicf(format interface{}, args ...interface{}) *MockEventLogger_Panicf_Call { + return &MockEventLogger_Panicf_Call{Call: _e.mock.On("Panicf", + append([]interface{}{format}, args...)...)} +} + +func (_c *MockEventLogger_Panicf_Call) Run(run func(format string, args ...interface{})) *MockEventLogger_Panicf_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 []interface{} + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Panicf_Call) Return() *MockEventLogger_Panicf_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Panicf_Call) RunAndReturn(run func(format string, args ...interface{})) *MockEventLogger_Panicf_Call { + _c.Run(run) + return _c +} + +// Warn provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Warn(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Warn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Warn' +type MockEventLogger_Warn_Call struct { + *mock.Call +} + +// Warn is a helper method to define mock.On call +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Warn(args ...interface{}) *MockEventLogger_Warn_Call { + return &MockEventLogger_Warn_Call{Call: _e.mock.On("Warn", + append([]interface{}{}, args...)...)} +} + +func (_c *MockEventLogger_Warn_Call) Run(run func(args ...interface{})) *MockEventLogger_Warn_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []interface{} + variadicArgs := make([]interface{}, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg0 = variadicArgs + run( + arg0..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Warn_Call) Return() *MockEventLogger_Warn_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Warn_Call) RunAndReturn(run func(args ...interface{})) *MockEventLogger_Warn_Call { + _c.Run(run) + return _c +} + +// Warnf provides a mock function for the type MockEventLogger +func (_mock *MockEventLogger) Warnf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _mock.Called(_ca...) + return +} + +// MockEventLogger_Warnf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Warnf' +type MockEventLogger_Warnf_Call struct { + *mock.Call +} + +// Warnf is a helper method to define mock.On call +// - format string +// - args ...interface{} +func (_e *MockEventLogger_Expecter) Warnf(format interface{}, args ...interface{}) *MockEventLogger_Warnf_Call { + return &MockEventLogger_Warnf_Call{Call: _e.mock.On("Warnf", + append([]interface{}{format}, args...)...)} +} + +func (_c *MockEventLogger_Warnf_Call) Run(run func(format string, args ...interface{})) *MockEventLogger_Warnf_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 []interface{} + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *MockEventLogger_Warnf_Call) Return() *MockEventLogger_Warnf_Call { + _c.Call.Return() + return _c +} + +func (_c *MockEventLogger_Warnf_Call) RunAndReturn(run func(format string, args ...interface{})) *MockEventLogger_Warnf_Call { + _c.Run(run) + return _c +} diff --git a/indexing-service/pkg/internal/extmocks/mock_IpniFinder.go b/indexing-service/pkg/internal/extmocks/mock_IpniFinder.go new file mode 100644 index 00000000..c96fdd2a --- /dev/null +++ b/indexing-service/pkg/internal/extmocks/mock_IpniFinder.go @@ -0,0 +1,108 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package extmocks + +import ( + "context" + + "github.com/ipni/go-libipni/find/model" + "github.com/multiformats/go-multihash" + mock "github.com/stretchr/testify/mock" +) + +// NewMockIpniFinder creates a new instance of MockIpniFinder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockIpniFinder(t interface { + mock.TestingT + Cleanup(func()) +}) *MockIpniFinder { + mock := &MockIpniFinder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockIpniFinder is an autogenerated mock type for the Finder type +type MockIpniFinder struct { + mock.Mock +} + +type MockIpniFinder_Expecter struct { + mock *mock.Mock +} + +func (_m *MockIpniFinder) EXPECT() *MockIpniFinder_Expecter { + return &MockIpniFinder_Expecter{mock: &_m.Mock} +} + +// Find provides a mock function for the type MockIpniFinder +func (_mock *MockIpniFinder) Find(context1 context.Context, multihash1 multihash.Multihash) (*model.FindResponse, error) { + ret := _mock.Called(context1, multihash1) + + if len(ret) == 0 { + panic("no return value specified for Find") + } + + var r0 *model.FindResponse + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash) (*model.FindResponse, error)); ok { + return returnFunc(context1, multihash1) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash) *model.FindResponse); ok { + r0 = returnFunc(context1, multihash1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.FindResponse) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, multihash.Multihash) error); ok { + r1 = returnFunc(context1, multihash1) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockIpniFinder_Find_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Find' +type MockIpniFinder_Find_Call struct { + *mock.Call +} + +// Find is a helper method to define mock.On call +// - context1 context.Context +// - multihash1 multihash.Multihash +func (_e *MockIpniFinder_Expecter) Find(context1 interface{}, multihash1 interface{}) *MockIpniFinder_Find_Call { + return &MockIpniFinder_Find_Call{Call: _e.mock.On("Find", context1, multihash1)} +} + +func (_c *MockIpniFinder_Find_Call) Run(run func(context1 context.Context, multihash1 multihash.Multihash)) *MockIpniFinder_Find_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 multihash.Multihash + if args[1] != nil { + arg1 = args[1].(multihash.Multihash) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockIpniFinder_Find_Call) Return(findResponse *model.FindResponse, err error) *MockIpniFinder_Find_Call { + _c.Call.Return(findResponse, err) + return _c +} + +func (_c *MockIpniFinder_Find_Call) RunAndReturn(run func(context1 context.Context, multihash1 multihash.Multihash) (*model.FindResponse, error)) *MockIpniFinder_Find_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/internal/extmocks/mock_IpniPublisher.go b/indexing-service/pkg/internal/extmocks/mock_IpniPublisher.go new file mode 100644 index 00000000..6645d348 --- /dev/null +++ b/indexing-service/pkg/internal/extmocks/mock_IpniPublisher.go @@ -0,0 +1,117 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package extmocks + +import ( + "context" + "iter" + + "github.com/ipni/go-libipni/metadata" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multihash" + mock "github.com/stretchr/testify/mock" +) + +// NewMockIpniPublisher creates a new instance of MockIpniPublisher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockIpniPublisher(t interface { + mock.TestingT + Cleanup(func()) +}) *MockIpniPublisher { + mock := &MockIpniPublisher{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockIpniPublisher is an autogenerated mock type for the AsyncPublisher type +type MockIpniPublisher struct { + mock.Mock +} + +type MockIpniPublisher_Expecter struct { + mock *mock.Mock +} + +func (_m *MockIpniPublisher) EXPECT() *MockIpniPublisher_Expecter { + return &MockIpniPublisher_Expecter{mock: &_m.Mock} +} + +// Publish provides a mock function for the type MockIpniPublisher +func (_mock *MockIpniPublisher) Publish(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta metadata.Metadata) error { + ret := _mock.Called(ctx, provider, contextID, digests, meta) + + if len(ret) == 0 { + panic("no return value specified for Publish") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, peer.AddrInfo, string, iter.Seq[multihash.Multihash], metadata.Metadata) error); ok { + r0 = returnFunc(ctx, provider, contextID, digests, meta) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockIpniPublisher_Publish_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Publish' +type MockIpniPublisher_Publish_Call struct { + *mock.Call +} + +// Publish is a helper method to define mock.On call +// - ctx context.Context +// - provider peer.AddrInfo +// - contextID string +// - digests iter.Seq[multihash.Multihash] +// - meta metadata.Metadata +func (_e *MockIpniPublisher_Expecter) Publish(ctx interface{}, provider interface{}, contextID interface{}, digests interface{}, meta interface{}) *MockIpniPublisher_Publish_Call { + return &MockIpniPublisher_Publish_Call{Call: _e.mock.On("Publish", ctx, provider, contextID, digests, meta)} +} + +func (_c *MockIpniPublisher_Publish_Call) Run(run func(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta metadata.Metadata)) *MockIpniPublisher_Publish_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 peer.AddrInfo + if args[1] != nil { + arg1 = args[1].(peer.AddrInfo) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 iter.Seq[multihash.Multihash] + if args[3] != nil { + arg3 = args[3].(iter.Seq[multihash.Multihash]) + } + var arg4 metadata.Metadata + if args[4] != nil { + arg4 = args[4].(metadata.Metadata) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) + }) + return _c +} + +func (_c *MockIpniPublisher_Publish_Call) Return(err error) *MockIpniPublisher_Publish_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockIpniPublisher_Publish_Call) RunAndReturn(run func(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta metadata.Metadata) error) *MockIpniPublisher_Publish_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/internal/jobwalker/jobwalker.go b/indexing-service/pkg/internal/jobwalker/jobwalker.go new file mode 100644 index 00000000..6bf1d856 --- /dev/null +++ b/indexing-service/pkg/internal/jobwalker/jobwalker.go @@ -0,0 +1,19 @@ +package jobwalker + +import "context" + +// WrappedState is a wrapper around any state to enable atomic access and modification +type WrappedState[State any] interface { + Access() State + Modify(func(State) State) + // CmpSwap calls the "willModify" function (potentially multiple times) and calls modify if it returns true + CmpSwap(willModify func(State) bool, modify func(State) State) bool +} + +// JobHandler handles the specified job and uses the passed in function to spawn more +// jobs. +// The handler should stop processing if spawn errors, returning the error from spawn +type JobHandler[Job any, State any] func(ctx context.Context, j Job, spawn func(Job) error, state WrappedState[State]) error + +// JobWalker processes a set of jobs that spawn other jobs, all while modifying a final state +type JobWalker[Job, State any] func(ctx context.Context, initial []Job, initialState State, handler JobHandler[Job, State]) (State, error) diff --git a/indexing-service/pkg/internal/jobwalker/parallelwalk/parallelwalk.go b/indexing-service/pkg/internal/jobwalker/parallelwalk/parallelwalk.go new file mode 100644 index 00000000..8bcb5c05 --- /dev/null +++ b/indexing-service/pkg/internal/jobwalker/parallelwalk/parallelwalk.go @@ -0,0 +1,141 @@ +package parallelwalk + +import ( + "context" + "errors" + "sync" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/jobwalker" +) + +type threadSafeState[State any] struct { + state State + lk sync.RWMutex +} + +func (ts *threadSafeState[State]) Access() State { + ts.lk.RLock() + defer ts.lk.RUnlock() + return ts.state +} + +func (ts *threadSafeState[State]) Modify(modify func(State) State) { + ts.lk.Lock() + defer ts.lk.Unlock() + ts.modify(modify) +} + +func (ts *threadSafeState[State]) modify(modify func(State) State) { + ts.state = modify(ts.state) +} + +func (ts *threadSafeState[State]) CmpSwap(willModify func(State) bool, modify func(State) State) bool { + if !willModify(ts.Access()) { + return false + } + ts.lk.Lock() + defer ts.lk.Unlock() + if !willModify(ts.state) { + return false + } + ts.modify(modify) + return true +} + +// NewParallelWalk generates a function to handle a series of jobs that may spawn more jobs +// It will execute jobs in parallel, with the specified concurrency until all initial jobs +// and all spawned jobs (recursively) are handled, or a job errors +// This code is adapted from https://github.com/ipfs/go-merkledag/blob/master/merkledag.go#L464C6-L584 +func NewParallelWalk[Job, State any](concurrency int) jobwalker.JobWalker[Job, State] { + return func(ctx context.Context, initial []Job, initialState State, handler jobwalker.JobHandler[Job, State]) (State, error) { + if len(initial) == 0 { + return initialState, errors.New("must provide at least one initial job") + } + jobFeed := make(chan Job) + spawnedJobs := make(chan Job) + jobFinishes := make(chan struct{}) + + state := &threadSafeState[State]{ + state: initialState, + } + var wg sync.WaitGroup + + errChan := make(chan error) + + jobFeedCtx, cancel := context.WithCancel(ctx) + + defer wg.Wait() + defer cancel() + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for job := range jobFeed { + + err := handler(jobFeedCtx, job, func(next Job) error { + select { + case spawnedJobs <- next: + return nil + case <-jobFeedCtx.Done(): + return jobFeedCtx.Err() + } + }, state) + + if err != nil { + select { + case errChan <- err: + case <-jobFeedCtx.Done(): + } + return + } + + select { + case jobFinishes <- struct{}{}: + case <-jobFeedCtx.Done(): + } + } + }() + } + defer close(jobFeed) + + jobProcessor := jobFeed + nextJob, queuedJobs := initial[0], initial[1:] + + var inProgress int + + hasNextJob := func() bool { + return jobProcessor != nil + } + + for { + select { + case jobProcessor <- nextJob: + inProgress++ + if len(queuedJobs) > 0 { + nextJob = queuedJobs[0] + queuedJobs = queuedJobs[1:] + } else { + var empty Job + nextJob = empty + jobProcessor = nil + } + case <-jobFinishes: + inProgress-- + if inProgress == 0 && !hasNextJob() { + return state.Access(), nil + } + case queued := <-spawnedJobs: + if !hasNextJob() { + nextJob = queued + jobProcessor = jobFeed + } else { + queuedJobs = append(queuedJobs, queued) + } + case err := <-errChan: + return state.Access(), err + case <-ctx.Done(): + return state.Access(), ctx.Err() + } + } + } +} diff --git a/indexing-service/pkg/internal/jobwalker/parallelwalk/parallelwalk_test.go b/indexing-service/pkg/internal/jobwalker/parallelwalk/parallelwalk_test.go new file mode 100644 index 00000000..20f46c90 --- /dev/null +++ b/indexing-service/pkg/internal/jobwalker/parallelwalk/parallelwalk_test.go @@ -0,0 +1,68 @@ +package parallelwalk + +import ( + "context" + "errors" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/jobwalker" + "github.com/stretchr/testify/assert" +) + +func TestParallelWalk(t *testing.T) { + concurrency := 3 + var parallelWalk = NewParallelWalk[int, []int](concurrency) + + initialJobs := []int{1, 2, 3, 4, 5} + + handler := func(ctx context.Context, j int, spawn func(int) error, state jobwalker.WrappedState[[]int]) error { + state.Modify(func(ints []int) []int { + return append(ints, j) + }) + + // jobs 2 and 4 spawn 3 new jobs each + if j == 2 || j == 4 { + if err := spawn(10 + j); err != nil { + return err + } + if err := spawn(20 + j); err != nil { + return err + } + if err := spawn(30 + j); err != nil { + return err + } + } + + return nil + } + + // happy path - all jobs, including spawned jobs, are processed as expected + t.Run("happy path", func(t *testing.T) { + state, err := parallelWalk(context.Background(), initialJobs, []int{}, handler) + + assert.NoError(t, err) + assert.Equal(t, 11, len(state)) + for _, job := range []int{1, 2, 3, 4, 5, 12, 22, 32, 14, 24, 34} { + assert.Contains(t, state, job) + } + }) + + // cancels as expected when the context is cancelled + t.Run("context cancelation", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := parallelWalk(ctx, initialJobs, []int{}, handler) + assert.Error(t, err) + assert.Equal(t, context.Canceled, err) + }) + + // returns err when the handler errors + t.Run("handler error", func(t *testing.T) { + handler := func(ctx context.Context, j int, spawn func(int) error, state jobwalker.WrappedState[[]int]) error { + return errors.New("test error") + } + _, err := parallelWalk(context.Background(), initialJobs, []int{}, handler) + assert.Error(t, err) + assert.Equal(t, "test error", err.Error()) + }) +} diff --git a/indexing-service/pkg/internal/jobwalker/singlewalk/singlewalk.go b/indexing-service/pkg/internal/jobwalker/singlewalk/singlewalk.go new file mode 100644 index 00000000..d45e5fd3 --- /dev/null +++ b/indexing-service/pkg/internal/jobwalker/singlewalk/singlewalk.go @@ -0,0 +1,55 @@ +package singlewalk + +import ( + "context" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/jobwalker" +) + +type singleState[State any] struct { + m State +} + +// Access implements jobwalker.WrappedState. +func (s *singleState[State]) Access() State { + return s.m +} + +// CmpSwap implements jobwalker.WrappedState. +func (s *singleState[State]) CmpSwap(willModify func(State) bool, modify func(State) State) bool { + if !willModify(s.m) { + return false + } + s.m = modify(s.m) + return true +} + +// Modify implements jobwalker.WrappedState. +func (s *singleState[State]) Modify(modify func(State) State) { + s.m = modify(s.m) +} + +var _ jobwalker.WrappedState[any] = &singleState[any]{} + +// SingleWalker processes jobs that span more jobs, sequentially depth first in a single thread +func SingleWalker[Job, State any](ctx context.Context, initial []Job, initialState State, handler jobwalker.JobHandler[Job, State]) (State, error) { + stack := initial + state := &singleState[State]{initialState} + for len(stack) > 0 { + select { + case <-ctx.Done(): + return state.Access(), ctx.Err() + default: + } + next := stack[len(stack)-1] + stack = stack[:len(stack)-1] + err := handler(ctx, next, func(j Job) error { + stack = append(stack, j) + return nil + }, state) + if err != nil { + return state.Access(), err + } + } + return state.Access(), nil +} diff --git a/indexing-service/pkg/internal/link/link.go b/indexing-service/pkg/internal/link/link.go new file mode 100644 index 00000000..92a92b4a --- /dev/null +++ b/indexing-service/pkg/internal/link/link.go @@ -0,0 +1,14 @@ +package link + +import ( + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" +) + +func ToCID(link ipld.Link) cid.Cid { + if cl, ok := link.(cidlink.Link); ok { + return cl.Cid + } + return cid.MustParse(link.String()) +} diff --git a/indexing-service/pkg/internal/revision/revision.go b/indexing-service/pkg/internal/revision/revision.go new file mode 100644 index 00000000..cdad4a44 --- /dev/null +++ b/indexing-service/pkg/internal/revision/revision.go @@ -0,0 +1,43 @@ +// Package revision provides the vsc revision, embedded by the compiler, as a +// global variable. +package revision + +import ( + "runtime/debug" +) + +// Revision returns the revision embedded by the compiler during build. +// Suffixed with "-dirty" if modified. +var Revision string + +func init() { + revision := "unknown" + var dirty bool + + // Get the revision from the build info + bi, ok := debug.ReadBuildInfo() + if !ok { + Revision = revision + return + } + + for _, bs := range bi.Settings { + switch bs.Key { + case "vcs.revision": + revision = bs.Value + if len(bs.Value) > 7 { + revision = bs.Value[:7] + } + case "vcs.modified": + if bs.Value == "true" { + dirty = true + } + } + } + + if dirty { + revision += "-dirty" + } + + Revision = revision +} diff --git a/indexing-service/pkg/internal/testutil/testutil.go b/indexing-service/pkg/internal/testutil/testutil.go new file mode 100644 index 00000000..44af20f3 --- /dev/null +++ b/indexing-service/pkg/internal/testutil/testutil.go @@ -0,0 +1,249 @@ +// Package testutil provides shared helpers for indexing service tests. +// +// It re-exports the ucantone testutil helpers (Alice, Bob, Service, RandomCID, +// RandomMultihash, RandomSigner, RandomDID, RandomBytes, Must, Must2, TestURL) +// and adds helpers for constructing self-signed UCAN invocations for the +// claims this service deals with (location, index, equals), random provider +// results, sharded DAG indexes, and structural equality assertions. +package testutil + +import ( + "bytes" + "math/rand" + "net" + "net/url" + "strconv" + "testing" + + "github.com/fil-forge/automobile" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/commands" + assertcaps "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + "github.com/fil-forge/ucantone/testutil" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + "github.com/ipni/go-libipni/find/model" + ipnimeta "github.com/ipni/go-libipni/metadata" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" + mh "github.com/multiformats/go-multihash" + multihash "github.com/multiformats/go-multihash/core" + "github.com/stretchr/testify/require" +) + +// Re-exports from ucantone/testutil for convenience and a single import point. +var ( + Alice = testutil.Alice + Bob = testutil.Bob + Carol = testutil.Carol + Mallory = testutil.Mallory + Service = testutil.Service + WebService = testutil.WebService + TestURL = testutil.TestURL + RandomBytes = testutil.RandomBytes + RandomCID = testutil.RandomCID + RandomMultihash = testutil.RandomDigest + RandomSigner = testutil.RandomSigner + RandomIssuer = testutil.RandomIssuer +) + +func Must[T any](val T, err error) func(*testing.T) T { return testutil.Must(val, err) } +func Must2[T, U any](v1 T, v2 U, err error) func(*testing.T) (T, U) { + return testutil.Must2(v1, v2, err) +} + +func RandomMultihashes(t *testing.T, count int) []mh.Multihash { + require.Greater(t, count, 0, "count must be greater than 0") + mhs := make([]mh.Multihash, 0, count) + for range count { + mhs = append(mhs, RandomMultihash(t)) + } + return mhs +} + +func RandomPeer(t *testing.T) peer.ID { + t.Helper() + _, pub, err := crypto.GenerateEd25519Key(nil) + require.NoError(t, err) + pid, err := peer.IDFromPublicKey(pub) + require.NoError(t, err) + return pid +} + +func RandomMultiaddr(t *testing.T) multiaddr.Multiaddr { + t.Helper() + addr := &net.TCPAddr{IP: net.IPv4(byte(rand.Intn(255)), byte(rand.Intn(255)), byte(rand.Intn(255)), byte(rand.Intn(255))), Port: rand.Intn(65535)} + maddr, err := manet.FromIP(addr.IP) + require.NoError(t, err) + port, err := multiaddr.NewComponent(multiaddr.ProtocolWithCode(multiaddr.P_TCP).Name, strconv.Itoa(addr.Port)) + require.NoError(t, err) + return multiaddr.Join(maddr, port) +} + +// RandomCAR creates a single-block CAR of random bytes and returns the root +// CID, hash of the CAR bytes, and the CAR bytes. +func RandomCAR(t *testing.T, size int) (cid.Cid, mh.Multihash, []byte) { + t.Helper() + data := RandomBytes(t, size) + root, err := cid.V1Builder{Codec: cid.Raw, MhType: multihash.SHA2_256}.Sum(data) + require.NoError(t, err) + var buf bytes.Buffer + w := automobile.NewWriter(&buf) + require.NoError(t, w.WriteHeader([]cid.Cid{root})) + require.NoError(t, w.WriteBlock(automobile.Block{Link: root, Data: data})) + carBytes := buf.Bytes() + carDigest, err := mh.Sum(carBytes, mh.SHA2_256, -1) + require.NoError(t, err) + return root, carDigest, carBytes +} + +// RandomLocationCommitment creates a self-signed /assert/location invocation +// (issuer == subject == a freshly-generated signer). +func RandomLocationCommitment(t *testing.T) ucan.Invocation { + t.Helper() + i := testutil.RandomIssuer(t) + return Must(assertcaps.Location.Invoke(i, i.DID(), &assertcaps.LocationArguments{ + Space: i.DID(), + Content: RandomMultihash(t), + Location: []commands.CborURL{commands.CborURL(*TestURL)}, + }))(t) +} + +// RandomIndexClaim creates a self-signed /assert/index invocation. +func RandomIndexClaim(t *testing.T) ucan.Invocation { + t.Helper() + i := testutil.RandomIssuer(t) + inv, err := assertcaps.Index.Invoke(i, i.DID(), &assertcaps.IndexArguments{ + Index: RandomCID(t), + }) + require.NoError(t, err) + return inv +} + +// RandomEqualsClaim creates a self-signed /assert/equals invocation. +func RandomEqualsClaim(t *testing.T) ucan.Invocation { + t.Helper() + i := testutil.RandomIssuer(t) + inv, err := assertcaps.Equals.Invoke(i, i.DID(), &assertcaps.EqualsArguments{ + Content: RandomMultihash(t), + Equals: RandomCID(t), + }) + require.NoError(t, err) + return inv +} + +// RandomShardedDagIndex creates a sharded DAG index with one shard containing +// a single slice of random bytes (size bytes). Returns the shard hash and the +// index. +func RandomShardedDagIndex(t *testing.T, size int) (mh.Multihash, blobindex.ShardedDagIndex) { + t.Helper() + shardHash := RandomMultihash(t) + sliceHash := RandomMultihash(t) + idx := blobindex.NewShardedDagIndex(1) + idx.SetSlice(shardHash, sliceHash, blobindex.Range{Start: 0, End: int64(size - 1)}) + return shardHash, idx +} + +// RandomProviderResult returns a randomly-populated IPNI provider result with +// no metadata. +func RandomProviderResult(t *testing.T) model.ProviderResult { + t.Helper() + return model.ProviderResult{ + ContextID: RandomBytes(t, 10), + Metadata: RandomBytes(t, 10), + Provider: &peer.AddrInfo{ + ID: RandomPeer(t), + Addrs: []multiaddr.Multiaddr{RandomMultiaddr(t), RandomMultiaddr(t)}, + }, + } +} + +// RandomBitswapProviderResult returns a provider result with bitswap metadata. +func RandomBitswapProviderResult(t *testing.T) model.ProviderResult { + t.Helper() + pr := RandomProviderResult(t) + b, err := ipnimeta.Bitswap{}.MarshalBinary() + require.NoError(t, err) + pr.Metadata = b + return pr +} + +// RandomIndexClaimProviderResult returns a provider result with index-claim metadata. +func RandomIndexClaimProviderResult(t *testing.T) model.ProviderResult { + t.Helper() + meta := metadata.IndexClaimMetadata{ + Index: RandomCID(t), + Expiration: 0, + Claim: RandomCID(t), + } + b, err := meta.MarshalBinary() + require.NoError(t, err) + pr := RandomProviderResult(t) + pr.Metadata = b + return pr +} + +// RandomLocationCommitmentProviderResult returns a provider result with +// location-commitment metadata. +func RandomLocationCommitmentProviderResult(t *testing.T) model.ProviderResult { + t.Helper() + shard := RandomCID(t) + meta := metadata.LocationCommitmentMetadata{ + Shard: &shard, + Range: &metadata.Range{Offset: 128}, + Expiration: 0, + Claim: RandomCID(t), + } + b, err := meta.MarshalBinary() + require.NoError(t, err) + pr := RandomProviderResult(t) + pr.Metadata = b + return pr +} + +// RequireEqualClaim compares two claims by their canonical encoded +// bytes (and thus their CID). If both are nil, this passes. +func RequireEqualClaim(t *testing.T, expected, actual ucan.Invocation) { + t.Helper() + if expected == nil { + require.Nil(t, actual) + return + } + require.NotNil(t, actual) + require.Equal(t, expected.Link(), actual.Link()) + require.Equal(t, expected.Bytes(), actual.Bytes(), "claim bytes differ") +} + +// RequireEqualIndex compares two sharded DAG indexes structurally. +func RequireEqualIndex(t *testing.T, expected, actual blobindex.ShardedDagIndex) { + t.Helper() + if expected == nil { + require.Nil(t, actual) + return + } + require.NotNil(t, actual) + require.NotZero(t, actual.Shards().Size()) + require.Equal(t, expected.Shards().Size(), actual.Shards().Size()) + for key, shard := range actual.Shards().Iterator() { + require.True(t, expected.Shards().Has(key)) + exp := expected.Shards().Get(key) + require.Equal(t, exp.Size(), shard.Size()) + for sliceMh, r := range shard.Iterator() { + require.True(t, exp.Has(sliceMh)) + require.Equal(t, exp.Get(sliceMh), r) + } + } +} + +// RandomLocalURL returns a random localhost URL useful as a service URL in +// tests where the actual port doesn't matter. +func RandomLocalURL(t *testing.T) url.URL { + t.Helper() + port := 3000 + rand.Intn(1000) + u, err := url.Parse("http://127.0.0.1:" + strconv.Itoa(port)) + require.NoError(t, err) + return *u +} diff --git a/indexing-service/pkg/pkg/internal/testutil/extmocks/mock_DynamoDBQueryClient.go b/indexing-service/pkg/pkg/internal/testutil/extmocks/mock_DynamoDBQueryClient.go new file mode 100644 index 00000000..866f092a --- /dev/null +++ b/indexing-service/pkg/pkg/internal/testutil/extmocks/mock_DynamoDBQueryClient.go @@ -0,0 +1,111 @@ +// Code generated by mockery v2.50.0. DO NOT EDIT. + +package extmocks + +import ( + context "context" + + dynamodb "github.com/aws/aws-sdk-go-v2/service/dynamodb" + mock "github.com/stretchr/testify/mock" +) + +// MockDynamoDBQueryClient is an autogenerated mock type for the QueryAPIClient type +type MockDynamoDBQueryClient struct { + mock.Mock +} + +type MockDynamoDBQueryClient_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDynamoDBQueryClient) EXPECT() *MockDynamoDBQueryClient_Expecter { + return &MockDynamoDBQueryClient_Expecter{mock: &_m.Mock} +} + +// Query provides a mock function with given fields: _a0, _a1, _a2 +func (_m *MockDynamoDBQueryClient) Query(_a0 context.Context, _a1 *dynamodb.QueryInput, _a2 ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) { + _va := make([]interface{}, len(_a2)) + for _i := range _a2 { + _va[_i] = _a2[_i] + } + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Query") + } + + var r0 *dynamodb.QueryOutput + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *dynamodb.QueryInput, ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error)); ok { + return rf(_a0, _a1, _a2...) + } + if rf, ok := ret.Get(0).(func(context.Context, *dynamodb.QueryInput, ...func(*dynamodb.Options)) *dynamodb.QueryOutput); ok { + r0 = rf(_a0, _a1, _a2...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dynamodb.QueryOutput) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *dynamodb.QueryInput, ...func(*dynamodb.Options)) error); ok { + r1 = rf(_a0, _a1, _a2...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDynamoDBQueryClient_Query_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Query' +type MockDynamoDBQueryClient_Query_Call struct { + *mock.Call +} + +// Query is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *dynamodb.QueryInput +// - _a2 ...func(*dynamodb.Options) +func (_e *MockDynamoDBQueryClient_Expecter) Query(_a0 interface{}, _a1 interface{}, _a2 ...interface{}) *MockDynamoDBQueryClient_Query_Call { + return &MockDynamoDBQueryClient_Query_Call{Call: _e.mock.On("Query", + append([]interface{}{_a0, _a1}, _a2...)...)} +} + +func (_c *MockDynamoDBQueryClient_Query_Call) Run(run func(_a0 context.Context, _a1 *dynamodb.QueryInput, _a2 ...func(*dynamodb.Options))) *MockDynamoDBQueryClient_Query_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]func(*dynamodb.Options), len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(func(*dynamodb.Options)) + } + } + run(args[0].(context.Context), args[1].(*dynamodb.QueryInput), variadicArgs...) + }) + return _c +} + +func (_c *MockDynamoDBQueryClient_Query_Call) Return(_a0 *dynamodb.QueryOutput, _a1 error) *MockDynamoDBQueryClient_Query_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDynamoDBQueryClient_Query_Call) RunAndReturn(run func(context.Context, *dynamodb.QueryInput, ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error)) *MockDynamoDBQueryClient_Query_Call { + _c.Call.Return(run) + return _c +} + +// NewMockDynamoDBQueryClient creates a new instance of MockDynamoDBQueryClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockDynamoDBQueryClient(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDynamoDBQueryClient { + mock := &MockDynamoDBQueryClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/indexing-service/pkg/pkg/internal/testutil/extmocks/mock_IpniFinder.go b/indexing-service/pkg/pkg/internal/testutil/extmocks/mock_IpniFinder.go new file mode 100644 index 00000000..5562fb13 --- /dev/null +++ b/indexing-service/pkg/pkg/internal/testutil/extmocks/mock_IpniFinder.go @@ -0,0 +1,98 @@ +// Code generated by mockery v2.50.0. DO NOT EDIT. + +package extmocks + +import ( + context "context" + + model "github.com/ipni/go-libipni/find/model" + mock "github.com/stretchr/testify/mock" + + multihash "github.com/multiformats/go-multihash" +) + +// MockIpniFinder is an autogenerated mock type for the Finder type +type MockIpniFinder struct { + mock.Mock +} + +type MockIpniFinder_Expecter struct { + mock *mock.Mock +} + +func (_m *MockIpniFinder) EXPECT() *MockIpniFinder_Expecter { + return &MockIpniFinder_Expecter{mock: &_m.Mock} +} + +// Find provides a mock function with given fields: _a0, _a1 +func (_m *MockIpniFinder) Find(_a0 context.Context, _a1 multihash.Multihash) (*model.FindResponse, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for Find") + } + + var r0 *model.FindResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, multihash.Multihash) (*model.FindResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, multihash.Multihash) *model.FindResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.FindResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, multihash.Multihash) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockIpniFinder_Find_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Find' +type MockIpniFinder_Find_Call struct { + *mock.Call +} + +// Find is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 multihash.Multihash +func (_e *MockIpniFinder_Expecter) Find(_a0 interface{}, _a1 interface{}) *MockIpniFinder_Find_Call { + return &MockIpniFinder_Find_Call{Call: _e.mock.On("Find", _a0, _a1)} +} + +func (_c *MockIpniFinder_Find_Call) Run(run func(_a0 context.Context, _a1 multihash.Multihash)) *MockIpniFinder_Find_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(multihash.Multihash)) + }) + return _c +} + +func (_c *MockIpniFinder_Find_Call) Return(_a0 *model.FindResponse, _a1 error) *MockIpniFinder_Find_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockIpniFinder_Find_Call) RunAndReturn(run func(context.Context, multihash.Multihash) (*model.FindResponse, error)) *MockIpniFinder_Find_Call { + _c.Call.Return(run) + return _c +} + +// NewMockIpniFinder creates a new instance of MockIpniFinder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockIpniFinder(t interface { + mock.TestingT + Cleanup(func()) +}) *MockIpniFinder { + mock := &MockIpniFinder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/indexing-service/pkg/presets/presets.go b/indexing-service/pkg/presets/presets.go new file mode 100644 index 00000000..b13acd2b --- /dev/null +++ b/indexing-service/pkg/presets/presets.go @@ -0,0 +1,18 @@ +package presets + +var IPNIFindURL = "https://cid.contact" + +var IPNIAnnounceURLs = []string{"https://cid.contact/announce"} + +var PrincipalMapping = map[string]string{ + "did:web:staging.up.warm.forge.fil.one": "z6MkpR58oZpK7L3cdZZciKT25ynGro7RZm6boFouWQ7AzF7v", + "did:web:up.forge.forge.fil.one": "z6MkgSttS3n3R56yGX2Eufvbwc58fphomhAsLoBCZpZJzQbr", + "did:web:up.test.forge.fil.one": "z6Mkpc2hW6sdhWNNx1BWHTmuCDP6wDBRVUiFnZt1SXMhUR24", + "did:web:staging.up.forge.fil.one": "z6MkhcbEpJpEvNVDd3n5RurquVdqs5dPU16JDU5VZTDtFgnn", + "did:web:up.forge.fil.one": "z6MkqdncRZ1wj8zxCTDUQ8CRT8NQWd63T7mZRvZUX8B7XDFi", + "did:web:staging.web3.storage": "z6MkhcbEpJpEvNVDd3n5RurquVdqs5dPU16JDU5VZTDtFgnn", + "did:web:web3.storage": "z6MkqdncRZ1wj8zxCTDUQ8CRT8NQWd63T7mZRvZUX8B7XDFi", + "did:web:staging.registrar.warm.forge.fil.one": "z6MkuQ8PfSMrzXCwZkbQv662nZC4FGGm1aucbH256HXXZyxo", + "did:web:registrar.forge.forge.fil.one": "z6MkkfWep96Dphp35s9VqSCD7h7G4R9R1QCR3K9TxpbSRrKf", + "did:web:registrar.test.forge.fil.one": "z6MksH3BJWkC7VbdwJYDvSc5amGtzia37zcf3jVNwJbrm6tp", +} diff --git a/indexing-service/pkg/providerresults/providerresults.go b/indexing-service/pkg/providerresults/providerresults.go new file mode 100644 index 00000000..14bd6904 --- /dev/null +++ b/indexing-service/pkg/providerresults/providerresults.go @@ -0,0 +1,89 @@ +// Package providerresults implements utilities for the IPNI provider result type +package providerresults + +import ( + "bytes" + // for importing schema + _ "embed" + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/ipld/go-ipld-prime/schema" + "github.com/ipni/go-libipni/find/model" + peer "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" +) + +var ( + //go:embed providerresults.ipldsch + providerResultsBytes []byte + peerIDConverter = bindnode.NamedBytesConverter("PeerID", bytesToPeerID, peerIDtoBytes) + multiaddrConverter = bindnode.NamedBytesConverter("Multiaddr", bytesToMultiaddr, multiaddrToBytes) + providerResultType schema.Type +) + +func init() { + typeSystem, err := ipld.LoadSchemaBytes(providerResultsBytes) + if err != nil { + panic(fmt.Errorf("failed to load schema: %w", err)) + } + providerResultType = typeSystem.TypeByName("ProviderResult") +} + +func bytesToPeerID(data []byte) (interface{}, error) { + if len(data) == 0 { + emptyID := peer.ID("") + return &emptyID, nil + } + + id, err := peer.IDFromBytes(data) + return &id, err +} + +func peerIDtoBytes(peerID interface{}) ([]byte, error) { + return []byte(*peerID.(*peer.ID)), nil +} + +func bytesToMultiaddr(data []byte) (interface{}, error) { + ma, err := multiaddr.NewMultiaddrBytes(data) + return &ma, err +} + +func multiaddrToBytes(ma interface{}) ([]byte, error) { + return (*ma.(*multiaddr.Multiaddr)).Bytes(), nil +} + +// UnmarshalCBOR decodes a provider result from CBOR-encoded bytes +func UnmarshalCBOR(data []byte) (model.ProviderResult, error) { + var pr model.ProviderResult + _, err := ipld.Unmarshal([]byte(data), dagcbor.Decode, &pr, providerResultType, peerIDConverter, multiaddrConverter) + if err != nil { + return model.ProviderResult{}, fmt.Errorf("unmarshaling provider result: %w", err) + } + return pr, nil +} + +// MarshalCBOR encodes a provider result in CBOR +func MarshalCBOR(providerResult model.ProviderResult) ([]byte, error) { + bytes, err := ipld.Marshal(dagcbor.Encode, &providerResult, providerResultType, peerIDConverter, multiaddrConverter) + if err != nil { + return nil, fmt.Errorf("marshaling provider result: %w", err) + } + return bytes, nil +} + +func equalProvider(a, b *peer.AddrInfo) bool { + if a == nil { + return b == nil + } + return b != nil && a.String() == b.String() +} + +// Equals compares two ProviderResults +func Equals(a, b model.ProviderResult) bool { + return bytes.Equal(a.ContextID, b.ContextID) && + bytes.Equal(a.Metadata, b.Metadata) && + equalProvider(a.Provider, b.Provider) +} diff --git a/indexing-service/pkg/providerresults/providerresults.ipldsch b/indexing-service/pkg/providerresults/providerresults.ipldsch new file mode 100644 index 00000000..73d237c8 --- /dev/null +++ b/indexing-service/pkg/providerresults/providerresults.ipldsch @@ -0,0 +1,16 @@ +type PeerID bytes +type Multiaddr bytes + +type Provider struct { + ID PeerID + Addrs [Multiaddr] +} representation tuple + +type ProviderResult struct { + # ContextID identifies the metadata that is part of this value. + ContextID Bytes + # Metadata contains information for the provider to use to retrieve data. + Metadata Bytes + # Provider is the peer ID and addresses of the provider. + Provider Provider +} representation tuple diff --git a/indexing-service/pkg/providerresults/providerresults_test.go b/indexing-service/pkg/providerresults/providerresults_test.go new file mode 100644 index 00000000..2b22481e --- /dev/null +++ b/indexing-service/pkg/providerresults/providerresults_test.go @@ -0,0 +1,86 @@ +package providerresults_test + +import ( + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/providerresults" + "github.com/ipni/go-libipni/find/model" + peer "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" +) + +func TestProviderResults__Equals(t *testing.T) { + testProvider := testutil.RandomProviderResult(t) + testProvider2 := testutil.RandomProviderResult(t) + + contextIDChanged := testProvider + contextIDChanged.ContextID = testutil.RandomBytes(t, 10) + + metadataChanged := testProvider + metadataChanged.Metadata = testutil.RandomBytes(t, 10) + + nullProvider := testProvider + nullProvider.Provider = nil + + providerPeerIDChanged := testProvider + providerPeerIDChanged.Provider = &peer.AddrInfo{ + ID: testutil.RandomPeer(t), + Addrs: testProvider.Provider.Addrs, + } + + providerAddrsChanged := testProvider + providerAddrsChanged.Provider = &peer.AddrInfo{ + ID: testProvider.Provider.ID, + Addrs: []multiaddr.Multiaddr{testutil.RandomMultiaddr(t), testutil.RandomMultiaddr(t)}, + } + + testCases := []struct { + name string + provider model.ProviderResult + assert require.BoolAssertionFunc + }{ + {"same provider", testProvider, require.True}, + {"full alternate", testProvider2, require.False}, + {"context ID changed", contextIDChanged, require.False}, + {"metadata changed", metadataChanged, require.False}, + {"provider changed to null", nullProvider, require.False}, + {"provider peer ID changed", providerPeerIDChanged, require.False}, + {"provider addrs changed", providerAddrsChanged, require.False}, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + testCase.assert(t, providerresults.Equals(testProvider, testCase.provider)) + }) + } +} + +func TestSerialization(t *testing.T) { + randomResult := testutil.RandomProviderResult(t) + + testCases := []struct { + name string + testResult model.ProviderResult + }{ + {name: "random result", testResult: randomResult}, + { + name: "empty peer ID", + testResult: model.ProviderResult{ + ContextID: randomResult.ContextID, + Metadata: randomResult.Metadata, + Provider: &peer.AddrInfo{ + ID: "", + Addrs: randomResult.Provider.Addrs, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + marshalled := testutil.Must(providerresults.MarshalCBOR(tc.testResult))(t) + unmarshalled := testutil.Must(providerresults.UnmarshalCBOR(marshalled))(t) + require.Equal(t, tc.testResult, unmarshalled) + }) + } +} diff --git a/indexing-service/pkg/redis/contentclaimsstore.go b/indexing-service/pkg/redis/contentclaimsstore.go new file mode 100644 index 00000000..acedb5a8 --- /dev/null +++ b/indexing-service/pkg/redis/contentclaimsstore.go @@ -0,0 +1,34 @@ +package redis + +import ( + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + cid "github.com/ipfs/go-cid" +) + +var _ types.ContentClaimsCache = (*ContentClaimsStore)(nil) + +// ContentClaimsStore is a RedisStore for storing content claims that implements types.ContentClaimsStore +type ContentClaimsStore = Store[cid.Cid, ucan.Invocation] + +// NewContentClaimsStore returns a new instance of a Content Claims Store using the given redis client +func NewContentClaimsStore(client Client, opts ...Option) *ContentClaimsStore { + return NewStore(claimFromRedis, claimToRedis, cidKeyString, client, opts...) +} + +func claimFromRedis(data string) (ucan.Invocation, error) { + return invocation.Decode([]byte(data)) +} + +func claimToRedis(claim ucan.Invocation) (string, error) { + data, err := invocation.Encode(claim) + if err != nil { + return "", err + } + return string(data), nil +} + +func cidKeyString(c cid.Cid) string { + return multihashKeyString(c.Hash()) +} diff --git a/indexing-service/pkg/redis/contentclaimsstore_test.go b/indexing-service/pkg/redis/contentclaimsstore_test.go new file mode 100644 index 00000000..99b0ea67 --- /dev/null +++ b/indexing-service/pkg/redis/contentclaimsstore_test.go @@ -0,0 +1,44 @@ +package redis_test + +import ( + "context" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/fil-forge/forge/protocol/commands" + assertcaps "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/stretchr/testify/require" +) + +func TestContentClaimsStore(t *testing.T) { + mockRedis := NewMockRedis() + contentClaimsStore := redis.NewContentClaimsStore(mockRedis) + + claim1, err := assertcaps.Location.Invoke( + testutil.Service, + testutil.Service.DID(), + &assertcaps.LocationArguments{ + Space: testutil.Service.DID(), + Content: testutil.RandomMultihash(t), + Location: []commands.CborURL{commands.CborURL(*testutil.TestURL)}, + }, + ) + require.NoError(t, err) + + claim2, err := assertcaps.Index.Invoke( + testutil.Service, + testutil.Service.DID(), + &assertcaps.IndexArguments{Index: testutil.RandomCID(t)}, + ) + require.NoError(t, err) + + ctx := context.Background() + require.NoError(t, contentClaimsStore.Set(ctx, claim1.Link(), claim1, false)) + require.NoError(t, contentClaimsStore.Set(ctx, claim2.Link(), claim2, true)) + + returnedClaim1 := testutil.Must(contentClaimsStore.Get(ctx, claim1.Link()))(t) + returnedClaim2 := testutil.Must(contentClaimsStore.Get(ctx, claim2.Link()))(t) + testutil.RequireEqualClaim(t, claim1, returnedClaim1) + testutil.RequireEqualClaim(t, claim2, returnedClaim2) +} diff --git a/indexing-service/pkg/redis/noproviderstore.go b/indexing-service/pkg/redis/noproviderstore.go new file mode 100644 index 00000000..ae523d5f --- /dev/null +++ b/indexing-service/pkg/redis/noproviderstore.go @@ -0,0 +1,48 @@ +package redis + +import ( + // imported for embedding + _ "embed" + "encoding/binary" + "errors" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/multiformats/go-multicodec" + multihash "github.com/multiformats/go-multihash" +) + +var ( + _ types.NoProviderStore = (*NoProviderStore)(nil) +) + +var ErrDecodingMulticodec = errors.New("error parsing multicodec") + +// NoProviderStore is a RedisStore for storing IPNI data that implements types.ProviderStore +type NoProviderStore = Store[multihash.Multihash, multicodec.Code] + +// NewNoProviderStore returns a new instance of an IPNI store using the given redis client +func NewNoProviderStore(client Client, opts ...Option) *NoProviderStore { + return NewStore(noProviderResultFromRedis, noProviderResultToRedis, noProviderMultihashKeyString, client, opts...) +} + +func noProviderResultFromRedis(data string) (multicodec.Code, error) { + code, read := binary.Uvarint([]byte(data)) + if read <= 0 { + return 0, ErrDecodingMulticodec + } + return multicodec.Code(code), nil +} + +func noProviderResultToRedis(record multicodec.Code) (string, error) { + buf := make([]byte, binary.MaxVarintLen64) + binary.PutUvarint(buf, uint64(record)) + return string(buf), nil +} + +// noProviderMultihashKeyString is a helper function to generate a Redis key for +// a multihash in the no provider cache. It prefixes the key with "no/" to +// distinguish it from the regular ProviderStore keys, in case the same Redis +// instance is being used for both. +func noProviderMultihashKeyString(k multihash.Multihash) string { + return "no/" + multihashKeyString(k) +} diff --git a/indexing-service/pkg/redis/noproviderstore_test.go b/indexing-service/pkg/redis/noproviderstore_test.go new file mode 100644 index 00000000..a25fe63d --- /dev/null +++ b/indexing-service/pkg/redis/noproviderstore_test.go @@ -0,0 +1,41 @@ +package redis_test + +import ( + "context" + "math/rand/v2" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +func TestNoProviderStore(t *testing.T) { + mockRedis := NewMockRedis() + noProviderStore := redis.NewNoProviderStore(mockRedis) + mh1, results1 := randomCodecList(t, 4) + mh2, results2 := randomCodecList(t, 4) + + ctx := context.Background() + _, err := noProviderStore.Add(ctx, mh1, results1...) + require.NoError(t, err) + _, err = noProviderStore.Add(ctx, mh2, results2...) + require.NoError(t, err) + + returnedResults1 := testutil.Must(noProviderStore.Members(ctx, mh1))(t) + returnedResults2 := testutil.Must(noProviderStore.Members(ctx, mh2))(t) + + require.ElementsMatch(t, results1, returnedResults1) + require.ElementsMatch(t, results2, returnedResults2) +} + +func randomCodecList(t *testing.T, num int) (multihash.Multihash, []multicodec.Code) { + randomHash := testutil.RandomCID(t).Hash() + codes := make([]multicodec.Code, 0, num) + for i := 0; i < num; i++ { + codes = append(codes, multicodec.Code(rand.Uint64())) + } + return randomHash, codes +} diff --git a/indexing-service/pkg/redis/providerstore.go b/indexing-service/pkg/redis/providerstore.go new file mode 100644 index 00000000..55e49385 --- /dev/null +++ b/indexing-service/pkg/redis/providerstore.go @@ -0,0 +1,36 @@ +package redis + +import ( + // imported for embedding + _ "embed" + + "github.com/fil-forge/forge/indexing-service/pkg/providerresults" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/ipni/go-libipni/find/model" + multihash "github.com/multiformats/go-multihash" +) + +var ( + _ types.ProviderStore = (*ProviderStore)(nil) +) + +// ProviderStore is a RedisStore for storing IPNI data that implements types.ProviderStore +type ProviderStore = BatchingValueSetStore[multihash.Multihash, model.ProviderResult] + +// NewProviderStore returns a new instance of an IPNI store using the given redis client +func NewProviderStore(client PipelineClient, opts ...Option) *ProviderStore { + return NewBatchingValueSetStore(providerResultFromRedis, providerResultToRedis, multihashKeyString, client, opts...) +} + +func providerResultFromRedis(data string) (model.ProviderResult, error) { + return providerresults.UnmarshalCBOR([]byte(data)) +} + +func providerResultToRedis(record model.ProviderResult) (string, error) { + data, err := providerresults.MarshalCBOR(record) + return string(data), err +} + +func multihashKeyString(k multihash.Multihash) string { + return string(k) +} diff --git a/indexing-service/pkg/redis/providerstore_test.go b/indexing-service/pkg/redis/providerstore_test.go new file mode 100644 index 00000000..ad6ee81a --- /dev/null +++ b/indexing-service/pkg/redis/providerstore_test.go @@ -0,0 +1,40 @@ +package redis_test + +import ( + "context" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/ipni/go-libipni/find/model" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +func TestProviderStore(t *testing.T) { + mockRedis := NewMockRedis() + providerStore := redis.NewProviderStore(mockRedis) + mh1, results1 := randomProviderResults(t, 4) + mh2, results2 := randomProviderResults(t, 4) + + ctx := context.Background() + _, err := providerStore.Add(ctx, mh1, results1...) + require.NoError(t, err) + _, err = providerStore.Add(ctx, mh2, results2...) + require.NoError(t, err) + + returnedResults1 := testutil.Must(providerStore.Members(ctx, mh1))(t) + returnedResults2 := testutil.Must(providerStore.Members(ctx, mh2))(t) + + require.ElementsMatch(t, results1, returnedResults1) + require.ElementsMatch(t, results2, returnedResults2) +} + +func randomProviderResults(t *testing.T, num int) (multihash.Multihash, []model.ProviderResult) { + randomHash := testutil.RandomCID(t).Hash() + providerResults := make([]model.ProviderResult, 0, num) + for i := 0; i < num; i++ { + providerResults = append(providerResults, testutil.RandomProviderResult(t)) + } + return randomHash, providerResults +} diff --git a/indexing-service/pkg/redis/redisstore.go b/indexing-service/pkg/redis/redisstore.go new file mode 100644 index 00000000..5dafb9d8 --- /dev/null +++ b/indexing-service/pkg/redis/redisstore.go @@ -0,0 +1,346 @@ +package redis + +import ( + "context" + "fmt" + "time" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/redis/go-redis/v9" +) + +// DefaultExpire is the expire time we set on Redis when Set/SetExpiration are called with expire=true +const DefaultExpire = time.Hour + +// Client is a subset of functions from the golang redis client that we need to implement our cache +type Client interface { + Get(context.Context, string) *redis.StringCmd + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd + SAdd(ctx context.Context, key string, members ...interface{}) *redis.IntCmd + SMembers(ctx context.Context, key string) *redis.StringSliceCmd + Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd + Persist(ctx context.Context, key string) *redis.BoolCmd +} + +// Pipelineable allows pipelines to be created for batching of write commands. +type Pipelineable interface { + Pipeline() Pipeliner +} + +// Pipeliner is a subset of functions from [redis.Pipeliner] that we need to +// implement pipelining for our cache. +type Pipeliner interface { + SAdd(ctx context.Context, key string, members ...any) *redis.IntCmd + Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd + Persist(ctx context.Context, key string) *redis.BoolCmd + Exec(ctx context.Context) ([]redis.Cmder, error) +} + +// PipelineClient is a client that also supports pipelining. +type PipelineClient interface { + Client + Pipelineable +} + +// Store wraps the go redis client to implement our general purpose cache interface, +// using the providedserialization/deserialization functions +type Store[Key, Value any] struct { + fromRedis func(string) (Value, error) + toRedis func(Value) (string, error) + keyString func(Key) string + client Client + config config +} + +var ( + _ Client = (*redis.Client)(nil) + _ types.Cache[any, any] = (*Store[any, any])(nil) +) + +type config struct { + expirationTime time.Duration +} + +func newConfig(opts []Option) config { + c := config{ + expirationTime: DefaultExpire, + } + for _, opt := range opts { + opt(&c) + } + return c +} + +type Option func(*config) + +func ExpirationTime(expirationTime time.Duration) Option { + return func(c *config) { + c.expirationTime = expirationTime + } +} + +// NewStore returns a new instance of a redis store with the provided serialization/deserialization functions +func NewStore[Key, Value any]( + fromRedis func(string) (Value, error), + toRedis func(Value) (string, error), + keyString func(Key) string, + client Client, + opts ...Option) *Store[Key, Value] { + return &Store[Key, Value]{fromRedis, toRedis, keyString, client, newConfig(opts)} +} + +// Get returns deserialized values from redis +func (rs *Store[Key, Value]) Get(ctx context.Context, key Key) (Value, error) { + data, err := rs.client.Get(ctx, rs.keyString(key)).Result() + + if err != nil { + var v Value + if err == redis.Nil { + return v, types.ErrKeyNotFound + } + return v, fmt.Errorf("error accessing redis: %w", err) + } + return rs.fromRedis(data) +} + +// Set saves a serialized value to redis +func (rs *Store[Key, Value]) Set(ctx context.Context, key Key, value Value, expires bool) error { + data, err := rs.toRedis(value) + if err != nil { + return err + } + duration := time.Duration(0) + if expires { + duration = rs.config.expirationTime + } + err = rs.client.Set(ctx, rs.keyString(key), data, duration).Err() + if err != nil { + return fmt.Errorf("error accessing redis: %w", err) + } + return nil +} + +// SetExpirable changes the expiration property for a given key +func (rs *Store[Key, Value]) SetExpirable(ctx context.Context, key Key, expires bool) error { + var err error + if expires { + err = rs.client.Expire(ctx, rs.keyString(key), rs.config.expirationTime).Err() + } else { + err = rs.client.Persist(ctx, rs.keyString(key)).Err() + } + if err != nil { + return fmt.Errorf("error accessing redis: %w", err) + } + return nil +} + +// Members returns all deserialized set values from redis. +// If the key does not exist, it returns ErrKeyNotFound. +func (rs *Store[Key, Value]) Members(ctx context.Context, key Key) ([]Value, error) { + data, err := rs.client.SMembers(ctx, rs.keyString(key)).Result() + if err != nil { + return nil, fmt.Errorf("getting set members: %w", err) + } + + // as opposed to other commands, SMembers doesn't return redis.Nil when the key doesn't exist, but an empty set + // this implementation assumes there is no need to differentiate between a non-existing key and an empty set + if len(data) == 0 { + return nil, types.ErrKeyNotFound + } + + var values []Value + for _, d := range data { + v, err := rs.fromRedis(d) + if err != nil { + return nil, err + } + values = append(values, v) + } + return values, nil +} + +// Add another value to the set of values for the given key. +func (rs *Store[Key, Value]) Add(ctx context.Context, key Key, values ...Value) (uint64, error) { + var data []any + for _, v := range values { + d, err := rs.toRedis(v) + if err != nil { + return 0, err + } + data = append(data, d) + } + n, err := rs.client.SAdd(ctx, rs.keyString(key), data...).Result() + if err != nil { + return 0, fmt.Errorf("adding set member: %w", err) + } + return uint64(n), nil +} + +type BatchingValueSetStore[K, V any] struct { + store *Store[K, V] + client PipelineClient + toRedis func(V) (string, error) + keyString func(K) string + opts []Option +} + +// NewBatchingValueSetStore creates a new value-set store (a store whose values +// are sets) that allows batching. +func NewBatchingValueSetStore[K, V any]( + fromRedis func(string) (V, error), + toRedis func(V) (string, error), + keyString func(K) string, + client PipelineClient, + opts ...Option, +) *BatchingValueSetStore[K, V] { + return &BatchingValueSetStore[K, V]{ + store: NewStore(fromRedis, toRedis, keyString, client, opts...), + client: client, + toRedis: toRedis, + keyString: keyString, + opts: opts, + } +} + +func (bvs *BatchingValueSetStore[K, V]) Add(ctx context.Context, key K, values ...V) (uint64, error) { + return bvs.store.Add(ctx, key, values...) +} + +func (bvs *BatchingValueSetStore[K, V]) SetExpirable(ctx context.Context, key K, expires bool) error { + return bvs.store.SetExpirable(ctx, key, expires) +} + +func (bvs *BatchingValueSetStore[K, V]) Members(ctx context.Context, key K) ([]V, error) { + return bvs.store.Members(ctx, key) +} + +func (bvs *BatchingValueSetStore[K, V]) Batch() types.ValueSetCacheBatcher[K, V] { + return NewPipelineBatcher(bvs.client.Pipeline(), bvs.toRedis, bvs.keyString, bvs.opts...) +} + +type PipelineBatcher[K, V any] struct { + toRedis func(V) (string, error) + keyString func(K) string + config config + pipeline Pipeliner +} + +func NewPipelineBatcher[K, V any]( + pipeline Pipeliner, + toRedis func(V) (string, error), + keyString func(K) string, + opts ...Option, +) *PipelineBatcher[K, V] { + batcher := PipelineBatcher[K, V]{ + toRedis: toRedis, + keyString: keyString, + config: newConfig(opts), + pipeline: pipeline, + } + return &batcher +} + +func (pb *PipelineBatcher[K, V]) Add(ctx context.Context, key K, values ...V) error { + var data []any + for _, v := range values { + d, err := pb.toRedis(v) + if err != nil { + return err + } + data = append(data, d) + } + pb.pipeline.SAdd(ctx, pb.keyString(key), data...) + return nil +} + +func (pb *PipelineBatcher[K, V]) SetExpirable(ctx context.Context, key K, expires bool) error { + if expires { + pb.pipeline.Expire(ctx, pb.keyString(key), pb.config.expirationTime) + } else { + pb.pipeline.Persist(ctx, pb.keyString(key)) + } + return nil +} + +func (pb *PipelineBatcher[K, V]) Commit(ctx context.Context) error { + _, err := pb.pipeline.Exec(ctx) + return err +} + +// NewClientAdapter converts a [redis.Client] into a [PipelineClient]. +func NewClientAdapter(client *redis.Client) PipelineClient { + return &clientAdapter{client} +} + +type clientAdapter struct { + client *redis.Client +} + +func (a *clientAdapter) Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd { + return a.client.Expire(ctx, key, expiration) +} + +func (a *clientAdapter) Get(ctx context.Context, key string) *redis.StringCmd { + return a.client.Get(ctx, key) +} + +func (a *clientAdapter) Persist(ctx context.Context, key string) *redis.BoolCmd { + return a.client.Persist(ctx, key) +} + +func (a *clientAdapter) Pipeline() Pipeliner { + return a.client.Pipeline() +} + +func (a *clientAdapter) SAdd(ctx context.Context, key string, members ...interface{}) *redis.IntCmd { + return a.client.SAdd(ctx, key, members...) +} + +func (a *clientAdapter) SMembers(ctx context.Context, key string) *redis.StringSliceCmd { + return a.client.SMembers(ctx, key) +} + +func (a *clientAdapter) Set(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd { + return a.client.Set(ctx, key, value, expiration) +} + +var _ PipelineClient = (*clientAdapter)(nil) + +// NewClientAdapter converts a [redis.ClusterClient] into a [PipelineClient]. +func NewClusterClientAdapter(client *redis.ClusterClient) PipelineClient { + return &clusterClientAdapter{client} +} + +type clusterClientAdapter struct { + client *redis.ClusterClient +} + +func (a *clusterClientAdapter) Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd { + return a.client.Expire(ctx, key, expiration) +} + +func (a *clusterClientAdapter) Get(ctx context.Context, key string) *redis.StringCmd { + return a.client.Get(ctx, key) +} + +func (a *clusterClientAdapter) Persist(ctx context.Context, key string) *redis.BoolCmd { + return a.client.Persist(ctx, key) +} + +func (a *clusterClientAdapter) Pipeline() Pipeliner { + return a.client.Pipeline() +} + +func (a *clusterClientAdapter) SAdd(ctx context.Context, key string, members ...interface{}) *redis.IntCmd { + return a.client.SAdd(ctx, key, members...) +} + +func (a *clusterClientAdapter) SMembers(ctx context.Context, key string) *redis.StringSliceCmd { + return a.client.SMembers(ctx, key) +} + +func (a *clusterClientAdapter) Set(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd { + return a.client.Set(ctx, key, value, expiration) +} + +var _ PipelineClient = (*clientAdapter)(nil) diff --git a/indexing-service/pkg/redis/redisstore_test.go b/indexing-service/pkg/redis/redisstore_test.go new file mode 100644 index 00000000..cbfa7e84 --- /dev/null +++ b/indexing-service/pkg/redis/redisstore_test.go @@ -0,0 +1,413 @@ +package redis_test + +import ( + "context" + "errors" + "fmt" + "maps" + "os" + "runtime" + "slices" + "strings" + "testing" + "time" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/fil-forge/forge/indexing-service/pkg/types" + goredis "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + valkey "github.com/testcontainers/testcontainers-go/modules/valkey" +) + +func TestRedisStore(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + opts []MockOption + behavior func(t *testing.T, store *redis.Store[string, string]) + finalState map[string]*redisValue + }{ + { + name: "normal behavior", + behavior: func(t *testing.T, store *redis.Store[string, string]) { + store.Set(ctx, "key1", "value1", true) + store.Set(ctx, "key2", "value2", false) + store.Set(ctx, "key3", "value3", true) + store.Set(ctx, "key4", "value4", false) + store.SetExpirable(ctx, "key3", false) + store.SetExpirable(ctx, "key4", true) + require.Equal(t, "value1", testutil.Must(store.Get(ctx, "key1"))(t)) + require.Equal(t, "value2", testutil.Must(store.Get(ctx, "key2"))(t)) + require.Equal(t, "value3", testutil.Must(store.Get(ctx, "key3"))(t)) + require.Equal(t, "value4", testutil.Must(store.Get(ctx, "key4"))(t)) + _, err := store.Get(ctx, "key5") + require.ErrorIs(t, err, types.ErrKeyNotFound) + }, + finalState: map[string]*redisValue{ + "key1": {map[string]struct{}{"value1": {}}, redis.DefaultExpire}, + "key2": {map[string]struct{}{"value2": {}}, 0}, + "key3": {map[string]struct{}{"value3": {}}, 0}, + "key4": {map[string]struct{}{"value4": {}}, redis.DefaultExpire}, + }, + }, + { + name: "get errors", + opts: []MockOption{WithErrorOnGet(errors.New("something went wrong"))}, + behavior: func(t *testing.T, store *redis.Store[string, string]) { + _, err := store.Get(ctx, "key1") + require.EqualError(t, err, "error accessing redis: something went wrong") + }, + }, + { + name: "set errors", + opts: []MockOption{WithErrorOnSet(errors.New("something went wrong"))}, + behavior: func(t *testing.T, store *redis.Store[string, string]) { + err := store.Set(ctx, "key1", "value1", true) + require.EqualError(t, err, "error accessing redis: something went wrong") + }, + }, + { + name: "set expiration errors", + opts: []MockOption{WithErrorOnSetExpiration(errors.New("something went wrong"))}, + behavior: func(t *testing.T, store *redis.Store[string, string]) { + err := store.SetExpirable(ctx, "key1", true) + require.EqualError(t, err, "error accessing redis: something went wrong") + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + mockRedis := NewMockRedis(testCase.opts...) + redisStore := redis.NewStore( + func(s string) (string, error) { return s, nil }, + func(s string) (string, error) { return s, nil }, + func(s string) string { return s }, + mockRedis) + testCase.behavior(t, redisStore) + expectedFinalState := testCase.finalState + if expectedFinalState == nil { + expectedFinalState = make(map[string]*redisValue) + } + require.Equal(t, expectedFinalState, mockRedis.data) + }) + } +} + +func TestRedisSetsStore(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + opts []MockOption + behavior func(t *testing.T, store *redis.Store[string, string]) + finalState map[string]*redisValue + }{ + { + name: "normal behavior", + behavior: func(t *testing.T, store *redis.Store[string, string]) { + n, err := store.Add(ctx, "key1", "value1", "value2") + require.NoError(t, err) + require.Equal(t, uint64(2), n) + + n, err = store.Add(ctx, "key1", "value3") + require.NoError(t, err) + require.Equal(t, uint64(1), n) + + n, err = store.Add(ctx, "key1", "value2") + require.NoError(t, err) + require.Equal(t, uint64(0), n) + + n, err = store.Add(ctx, "key2", "value4") + require.NoError(t, err) + require.Equal(t, uint64(1), n) + + n, err = store.Add(ctx, "key3", "value5") + require.NoError(t, err) + require.Equal(t, uint64(1), n) + + n, err = store.Add(ctx, "key4", "value6") + require.NoError(t, err) + require.Equal(t, uint64(1), n) + + err = store.SetExpirable(ctx, "key3", true) + require.NoError(t, err) + + err = store.SetExpirable(ctx, "key3", false) + require.NoError(t, err) + + err = store.SetExpirable(ctx, "key4", true) + require.NoError(t, err) + + require.ElementsMatch(t, []string{"value1", "value2", "value3"}, testutil.Must(store.Members(ctx, "key1"))(t)) + require.ElementsMatch(t, []string{"value4"}, testutil.Must(store.Members(ctx, "key2"))(t)) + require.ElementsMatch(t, []string{"value5"}, testutil.Must(store.Members(ctx, "key3"))(t)) + require.ElementsMatch(t, []string{"value6"}, testutil.Must(store.Members(ctx, "key4"))(t)) + _, err = store.Members(ctx, "key5") + require.ErrorIs(t, err, types.ErrKeyNotFound) + }, + finalState: map[string]*redisValue{ + "key1": {map[string]struct{}{"value1": {}, "value2": {}, "value3": {}}, 0}, + "key2": {map[string]struct{}{"value4": {}}, 0}, + "key3": {map[string]struct{}{"value5": {}}, 0}, + "key4": {map[string]struct{}{"value6": {}}, redis.DefaultExpire}, + }, + }, + { + name: "members errors", + opts: []MockOption{WithErrorOnGet(errors.New("something went wrong"))}, + behavior: func(t *testing.T, store *redis.Store[string, string]) { + _, err := store.Members(ctx, "key1") + require.EqualError(t, err, "getting set members: something went wrong") + }, + }, + { + name: "add errors", + opts: []MockOption{WithErrorOnAdd(errors.New("something went wrong"))}, + behavior: func(t *testing.T, store *redis.Store[string, string]) { + _, err := store.Add(ctx, "key1", "value1") + require.EqualError(t, err, "adding set member: something went wrong") + }, + }, + { + name: "set expiration errors", + opts: []MockOption{WithErrorOnSetExpiration(errors.New("something went wrong"))}, + behavior: func(t *testing.T, store *redis.Store[string, string]) { + err := store.SetExpirable(ctx, "key1", true) + require.EqualError(t, err, "error accessing redis: something went wrong") + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + mockRedis := NewMockRedis(testCase.opts...) + redisStore := redis.NewStore( + func(s string) (string, error) { return s, nil }, + func(s string) (string, error) { return s, nil }, + func(s string) string { return s }, + mockRedis) + testCase.behavior(t, redisStore) + expectedFinalState := testCase.finalState + if expectedFinalState == nil { + expectedFinalState = make(map[string]*redisValue) + } + require.Equal(t, expectedFinalState, mockRedis.data) + }) + } +} + +type redisValue struct { + data map[string]struct{} + expires time.Duration +} + +type MockRedis struct { + data map[string]*redisValue + errGet error + errSet error + errAdd error + errSetExpiration error +} + +var _ redis.Client = (*MockRedis)(nil) + +type MockOption func(*MockRedis) + +func WithErrorOnGet(err error) MockOption { + return func(m *MockRedis) { + m.errGet = err + } +} + +func WithErrorOnSet(err error) MockOption { + return func(m *MockRedis) { + m.errSet = err + } +} + +func WithErrorOnAdd(err error) MockOption { + return func(m *MockRedis) { + m.errAdd = err + } +} + +func WithErrorOnSetExpiration(err error) MockOption { + return func(m *MockRedis) { + m.errSetExpiration = err + } +} + +func NewMockRedis(opts ...MockOption) *MockRedis { + m := &MockRedis{data: make(map[string]*redisValue)} + for _, opt := range opts { + opt(m) + } + return m +} + +// Expire implements redis.RedisClient. +func (m *MockRedis) Expire(ctx context.Context, key string, expiration time.Duration) *goredis.BoolCmd { + cmd := goredis.NewBoolCmd(ctx, nil) + if m.errSetExpiration != nil { + cmd.SetErr(m.errSetExpiration) + return cmd + } + val, ok := m.data[key] + if ok && val.expires != expiration { + val.expires = expiration + cmd.SetVal(true) + } + return cmd +} + +// Get implements redis.RedisClient. +func (m *MockRedis) Get(ctx context.Context, key string) *goredis.StringCmd { + cmd := goredis.NewStringCmd(ctx, nil) + if m.errGet != nil { + cmd.SetErr(m.errGet) + return cmd + } + val, ok := m.data[key] + if !ok { + cmd.SetErr(goredis.Nil) + } else { + for k := range val.data { + cmd.SetVal(k) + break + } + } + return cmd +} + +// Persist implements redis.RedisClient. +func (m *MockRedis) Persist(ctx context.Context, key string) *goredis.BoolCmd { + cmd := goredis.NewBoolCmd(ctx, nil) + if m.errSetExpiration != nil { + cmd.SetErr(m.errSetExpiration) + return cmd + } + val, ok := m.data[key] + if ok && val.expires != 0 { + val.expires = 0 + cmd.SetVal(true) + } + return cmd +} + +// Set implements redis.RedisClient. +func (m *MockRedis) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *goredis.StatusCmd { + cmd := goredis.NewStatusCmd(ctx, nil) + if m.errSet != nil { + cmd.SetErr(m.errSet) + return cmd + } + data := map[string]struct{}{value.(string): {}} + m.data[key] = &redisValue{data, expiration} + return cmd +} + +// SAdd implements redis.RedisClient. +func (m *MockRedis) SAdd(ctx context.Context, key string, values ...interface{}) *goredis.IntCmd { + cmd := goredis.NewIntCmd(ctx, nil) + if m.errAdd != nil { + cmd.SetErr(m.errAdd) + return cmd + } + data := map[string]struct{}{} + val, ok := m.data[key] + if ok { + data = val.data + } + written := uint64(0) + for _, v := range values { + _, ok := data[v.(string)] + if !ok { + data[v.(string)] = struct{}{} + written++ + } + } + m.data[key] = &redisValue{data, 0} + cmd.SetVal(int64(written)) + return cmd +} + +// SMembers implements redis.RedisClient. +func (m *MockRedis) SMembers(ctx context.Context, key string) *goredis.StringSliceCmd { + cmd := goredis.NewStringSliceCmd(ctx, nil) + if m.errGet != nil { + cmd.SetErr(m.errGet) + return cmd + } + val, ok := m.data[key] + if !ok { + // SMembers returns an empty set for non-existing keys + cmd.SetVal([]string{}) + } else { + values := slices.Collect(maps.Keys(val.data)) + cmd.SetVal(values) + } + return cmd +} + +func (m *MockRedis) Pipeline() redis.Pipeliner { + panic("not implemented") +} + +func TestBatchingValueSetStore(t *testing.T) { + if os.Getenv("CI") != "" && runtime.GOOS != "linux" { + t.SkipNow() + } + + ctx := context.Background() + container, err := valkey.Run(ctx, + "valkey/valkey:7.2.5", + valkey.WithSnapshotting(10, 1), + valkey.WithLogLevel(valkey.LogLevelVerbose), + ) + testcontainers.CleanupContainer(t, container) + require.NoError(t, err) + + uri, err := container.ConnectionString(ctx) + require.NoError(t, err) + + opts := &goredis.Options{Addr: strings.TrimPrefix(uri, "redis://")} + client := redis.NewClientAdapter(goredis.NewClient(opts)) + + store := redis.NewBatchingValueSetStore( + func(s string) (string, error) { return s, nil }, + func(s string) (string, error) { return s, nil }, + func(s string) string { return s }, + client, + ) + type TestData struct { + key string + value string + } + var testdata []TestData + for i := range 1000 { + testdata = append(testdata, TestData{ + key: fmt.Sprintf("key%d", i), + value: fmt.Sprintf("val%d", i), + }) + } + + batch := store.Batch() + for _, d := range testdata { + err := batch.Add(ctx, d.key, d.value) + require.NoError(t, err) + + err = batch.SetExpirable(ctx, d.key, false) + require.NoError(t, err) + } + + err = batch.Commit(ctx) + require.NoError(t, err) + + // verify everything was set + for _, d := range testdata { + vals, err := store.Members(ctx, d.key) + require.NoError(t, err) + require.Len(t, vals, 1) + require.Equal(t, d.value, vals[0]) + } +} diff --git a/indexing-service/pkg/redis/shardeddagindexstore_test.go b/indexing-service/pkg/redis/shardeddagindexstore_test.go new file mode 100644 index 00000000..6e9bc6f1 --- /dev/null +++ b/indexing-service/pkg/redis/shardeddagindexstore_test.go @@ -0,0 +1,36 @@ +package redis_test + +import ( + "context" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/redis" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestShardedDagIndexStore(t *testing.T) { + mockRedis := NewMockRedis() + shardedDagIndexStore := redis.NewShardedDagIndexStore(mockRedis) + hash1, index1 := testutil.RandomShardedDagIndex(t, 32) + hash2, index2 := testutil.RandomShardedDagIndex(t, 32) + + aliceDid := testutil.Alice.DID() + encodedID1 := testutil.Must(types.ContextID{ + Space: &aliceDid, + Hash: hash1, + }.ToEncoded())(t) + encodedID2 := testutil.Must(types.ContextID{ + Hash: hash2, + }.ToEncoded())(t) + + ctx := context.Background() + require.NoError(t, shardedDagIndexStore.Set(ctx, encodedID1, index1, false)) + require.NoError(t, shardedDagIndexStore.Set(ctx, encodedID2, index2, true)) + + returnedIndex1 := testutil.Must(shardedDagIndexStore.Get(ctx, encodedID1))(t) + returnedIndex2 := testutil.Must(shardedDagIndexStore.Get(ctx, encodedID2))(t) + testutil.RequireEqualIndex(t, index1, returnedIndex1) + testutil.RequireEqualIndex(t, index2, returnedIndex2) +} diff --git a/indexing-service/pkg/redis/shareddagindexstore.go b/indexing-service/pkg/redis/shareddagindexstore.go new file mode 100644 index 00000000..065492c3 --- /dev/null +++ b/indexing-service/pkg/redis/shareddagindexstore.go @@ -0,0 +1,36 @@ +package redis + +import ( + "bytes" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" +) + +var ( + _ types.ShardedDagIndexStore = (*ShardedDagIndexStore)(nil) +) + +// ShardedDagIndexStore is a RedisStore for storing sharded dag indexes that implements types.ShardedDagIndexStore +type ShardedDagIndexStore = Store[types.EncodedContextID, blobindex.ShardedDagIndex] + +// NewShardedDagIndexStore returns a new instance of a ShardedDagIndex store using the given redis client +func NewShardedDagIndexStore(client Client, opts ...Option) *ShardedDagIndexStore { + return NewStore(shardedDagIndexFromRedis, shardedDagIndexToRedis, encodedContextIDKeyString, client, opts...) +} + +func shardedDagIndexFromRedis(data string) (blobindex.ShardedDagIndex, error) { + return blobindex.Extract(bytes.NewReader([]byte(data))) +} + +func shardedDagIndexToRedis(shardedDagIndex blobindex.ShardedDagIndex) (string, error) { + var buf bytes.Buffer + if err := blobindex.Archive(shardedDagIndex, &buf); err != nil { + return "", err + } + return buf.String(), nil +} + +func encodedContextIDKeyString(encodedContextID types.EncodedContextID) string { + return string(encodedContextID) +} diff --git a/indexing-service/pkg/server/server.go b/indexing-service/pkg/server/server.go new file mode 100644 index 00000000..1047a1fa --- /dev/null +++ b/indexing-service/pkg/server/server.go @@ -0,0 +1,417 @@ +package server + +import ( + "compress/gzip" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/fil-forge/automobile" + "github.com/fil-forge/forge/indexing-service/pkg/build" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/internal/identity" + assertcaps "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/retrieval" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/multikey" + "github.com/fil-forge/ucantone/multikey/ed25519" + "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" + logging "github.com/ipfs/go-log/v2" + "github.com/ipni/go-libipni/find/model" + "github.com/ipni/go-libipni/metadata" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multibase" + "github.com/multiformats/go-multihash" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +var log = logging.Logger("server") + +type ipniConfig struct { + provider peer.AddrInfo + metadata []byte +} + +type config struct { + id identity.Identity + contentClaimsOptions []server.HTTPOption + enableTelemetry bool + ipniConfig *ipniConfig +} + +type Option func(*config) error + +// WithIdentity specifies the server DID. +func WithIdentity(id identity.Identity) Option { + return func(c *config) error { + c.id = id + return nil + } +} + +func WithContentClaimsOptions(options ...server.HTTPOption) Option { + return func(c *config) error { + c.contentClaimsOptions = options + return nil + } +} + +func WithTelemetry() Option { + return func(c *config) error { + c.enableTelemetry = true + return nil + } +} + +func WithIPNI(provider peer.AddrInfo, metadata metadata.Metadata) Option { + return func(c *config) error { + mb, err := metadata.MarshalBinary() + if err != nil { + return err + } + c.ipniConfig = &ipniConfig{ + provider: provider, + metadata: mb, + } + return nil + } +} + +// ListenAndServe creates a new indexing service HTTP server, and starts it up. +func ListenAndServe(addr string, indexer types.Service, opts ...Option) error { + mux, err := NewServer(indexer, opts...) + if err != nil { + return err + } + srv := &http.Server{ + Addr: addr, + Handler: mux, + } + log.Infof("Listening on %s", addr) + err = srv.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil +} + +// NewServer creates a new indexing service HTTP server. +func NewServer(indexer types.Service, opts ...Option) (*http.ServeMux, error) { + c := &config{} + for _, opt := range opts { + if err := opt(c); err != nil { + return nil, err + } + } + + if c.id.Issuer == nil { + log.Warn("Generating a server identity as one has not been set!") + signer, err := ed25519.Generate() + if err != nil { + return nil, fmt.Errorf("generating identity: %w", err) + } + c.id = identity.Identity{ + Issuer: multikey.KeyIssuer(signer), + } + } + + log.Infof("Server ID: %s", c.id) + + mux := http.NewServeMux() + maybeInstrumentAndAdd(mux, "GET /", GetRootHandler(c.id), c.enableTelemetry) + maybeInstrumentAndAdd(mux, "GET /claim/{claim}", GetClaimHandler(indexer), c.enableTelemetry) + // temporary fix: post claims handler accessible at POST / too + maybeInstrumentAndAdd(mux, "POST /", PostClaimsHandler(c.id, indexer, c.contentClaimsOptions...), c.enableTelemetry) + maybeInstrumentAndAdd(mux, "POST /claims", PostClaimsHandler(c.id, indexer, c.contentClaimsOptions...), c.enableTelemetry) + maybeInstrumentAndAdd(mux, "GET /claims", withGzip(GetClaimsHandler(indexer)), c.enableTelemetry) + maybeInstrumentAndAdd(mux, "GET /.well-known/did.json", GetDIDDocument(c.id), c.enableTelemetry) + if c.ipniConfig != nil { + maybeInstrumentAndAdd(mux, "GET /cid/{cid}", GetIPNICIDHandler(indexer, c.ipniConfig), c.enableTelemetry) + } + return mux, nil +} + +func maybeInstrumentAndAdd(mux *http.ServeMux, route string, handler http.HandlerFunc, enableTelemetry bool) { + if enableTelemetry { + mux.Handle(route, otelhttp.NewHandler(handler, route, otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents))) + } else { + mux.HandleFunc(route, handler) + } +} + +// gzipResponseWriter wraps http.ResponseWriter to support gzip compression +type gzipResponseWriter struct { + io.Writer + http.ResponseWriter +} + +func (w gzipResponseWriter) Write(b []byte) (int, error) { + return w.Writer.Write(b) +} + +// withGzip wraps a handler to support gzip compression if the client accepts it +func withGzip(handler http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Check if client accepts gzip encoding + if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + handler(w, r) + return + } + + // Set the content encoding header + w.Header().Set("Content-Encoding", "gzip") + + // Create gzip writer + gz := gzip.NewWriter(w) + defer gz.Close() + + // Wrap the response writer + gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w} + handler(gzw, r) + } +} + +// GetRootHandler displays version info when a GET request is sent to "/". +func GetRootHandler(id ucan.Issuer) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf("🔥 indexing-service %s\n", build.Version))) + w.Write([]byte("- https://github.com/fil-forge/forge/indexing-service\n")) + w.Write([]byte(fmt.Sprintf("- %s\n", id))) + } +} + +// GetClaimHandler retrieves a single content claim by it's root CID. +func GetClaimHandler(service types.Getter) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, "/") + c, err := cid.Parse(parts[len(parts)-1]) + if err != nil { + http.Error(w, fmt.Sprintf("invalid CID: %s", err), http.StatusBadRequest) + return + } + + claim, err := service.Get(r.Context(), c) + if err != nil { + if errors.Is(err, types.ErrKeyNotFound) { + http.Error(w, fmt.Sprintf("not found: %s", c), http.StatusNotFound) + return + } + log.Errorf("getting claim: %s", err) + http.Error(w, "failed to get claim", http.StatusInternalServerError) + return + } + + _, err = w.Write(claim.Bytes()) + if err != nil { + log.Warnf("serving claim: %s: %s", c, err) + } + } +} + +// PostClaimsHandler invokes the ucanto service when a POST request is sent to +// "/claims". +func PostClaimsHandler(id ucan.Issuer, service types.Publisher, options ...server.HTTPOption) http.HandlerFunc { + server, err := contentclaims.NewUCANServer(id, service, options...) + if err != nil { + log.Fatalf("creating ucanto server: %s", err) + } + return server.ServeHTTP +} + +// GetClaimsHandler retrieves content claims when a GET request is sent to +// "/claims?multihash={multihash}". +func GetClaimsHandler(service types.Querier) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, s := telemetry.StartSpan(r.Context(), "GetClaimsHandler") + defer s.End() + + queryTypeParam := r.URL.Query()["type"] + var queryType types.QueryType + switch len(queryTypeParam) { + case 0: + queryType = types.QueryTypeStandard + case 1: + var err error + queryType, err = types.ParseQueryType(queryTypeParam[0]) + if err != nil { + http.Error(w, fmt.Sprint(err), http.StatusBadRequest) + return + } + default: + http.Error(w, fmt.Sprintf("only one 'type' parameter is allowed, but got %d", len(queryTypeParam)), http.StatusBadRequest) + return + } + + mhStrings := r.URL.Query()["multihash"] + hashes := make([]multihash.Multihash, 0, len(mhStrings)) + for _, mhString := range mhStrings { + _, bytes, err := multibase.Decode(mhString) + if err != nil { + http.Error(w, fmt.Sprintf("invalid multibase encoding: %s", err.Error()), http.StatusBadRequest) + return + } + hashes = append(hashes, bytes) + } + if len(hashes) == 0 { + http.Error(w, "missing digests", 400) + return + } + + spaceStrings := r.URL.Query()["spaces"] + spaces := make([]did.DID, 0, len(spaceStrings)) + for _, spaceString := range spaceStrings { + space, err := did.Parse(spaceString) + if err != nil { + http.Error(w, fmt.Sprintf("invalid did: %s", err.Error()), http.StatusBadRequest) + return + } + spaces = append(spaces, space) + } + + var dlgs []ucan.Delegation + agentMsgHeader := r.Header.Get(retrieval.HTTPHeaderName) + if agentMsgHeader != "" { + msg, err := container.Decode([]byte(agentMsgHeader)) + if err != nil { + http.Error(w, fmt.Sprintf("decoding agent message: %s", err.Error()), http.StatusBadRequest) + return + } + dlgs = msg.Delegations() + } + + qr, err := service.Query(ctx, types.Query{ + Type: queryType, + Hashes: hashes, + Match: types.Match{ + Subject: spaces, + }, + Delegations: dlgs, + }) + if err != nil { + http.Error(w, fmt.Sprintf("processing query: %s", err.Error()), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + carWriter := automobile.NewWriter(w) + err = carWriter.WriteHeader([]cid.Cid{qr.Root().Link}) + if err != nil { + log.Errorf("writing CAR header: %s", err) + return + } + for _, block := range qr.Blocks() { + if err := carWriter.WriteBlock(automobile.Block(block)); err != nil { + log.Errorf("writing CAR block: %s", err) + return + } + } + } +} + +func GetIPNICIDHandler(service types.Querier, config *ipniConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, s := telemetry.StartSpan(r.Context(), "GetClaimsHandler") + defer s.End() + if config == nil { + http.Error(w, "IPNI config not available", http.StatusInternalServerError) + return + } + parts := strings.Split(r.URL.Path, "/") + c, err := cid.Parse(parts[len(parts)-1]) + if err != nil { + http.Error(w, fmt.Sprintf("invalid CID: %s", err), http.StatusBadRequest) + return + } + mh := c.Hash() + qr, err := service.Query(ctx, types.Query{ + Type: types.QueryTypeStandard, + Hashes: []multihash.Multihash{mh}, + Match: types.Match{ + Subject: []did.DID{}, + }, + }) + if err != nil { + http.Error(w, fmt.Sprintf("processing query: %s", err.Error()), http.StatusInternalServerError) + return + } + if len(qr.Claims()) == 0 { + http.Error(w, fmt.Sprintf("no claims found for CID: %s", c), http.StatusNotFound) + return + } + + blocks := map[cid.Cid][]byte{} + for _, block := range qr.Blocks() { + blocks[block.Link] = block.Data + } + + // iterate over all claims to see if there are location claims, return preset peer if found + for _, root := range qr.Claims() { + data, ok := blocks[root] + if !ok { + http.Error(w, fmt.Sprintf("missing claim: %s:", c), http.StatusInternalServerError) + continue + } + claim, err := invocation.Decode(data) + if err != nil { + http.Error(w, fmt.Sprintf("decoding claim: %s", err), http.StatusInternalServerError) + return + } + + switch claim.Command() { + case assertcaps.Location.Command: + data, err := model.MarshalFindResponse(&model.FindResponse{ + MultihashResults: []model.MultihashResult{{ + Multihash: mh, + ProviderResults: []model.ProviderResult{{ + ContextID: mh, + Metadata: config.metadata, + Provider: &config.provider, + }}, + }}, + }) + + if err != nil { + http.Error(w, fmt.Sprintf("marshalling find response: %s", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(data) + if err != nil { + log.Errorf("sending find response: %s", err) + } + return + } + } + http.Error(w, fmt.Sprintf("no claims found for CID: %s", c), http.StatusNotFound) + } +} + +// GetDIDDocument returns the DID document for did:web resolution. +func GetDIDDocument(issuer multikey.Issuer) http.HandlerFunc { + id := identity.Identity{Issuer: issuer} + return func(w http.ResponseWriter, r *http.Request) { + doc, err := id.DIDDocument() + if err != nil { + http.Error(w, fmt.Sprintf("getting DID document: %s", err), http.StatusInternalServerError) + return + } + w.Header().Add("Content-Type", "application/json") + bytes, err := json.Marshal(doc) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Write(bytes) + } +} diff --git a/indexing-service/pkg/server/server_test.go b/indexing-service/pkg/server/server_test.go new file mode 100644 index 00000000..9b3739e2 --- /dev/null +++ b/indexing-service/pkg/server/server_test.go @@ -0,0 +1,93 @@ +package server + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/multikey" + "github.com/ipfs/go-datastore" + "github.com/stretchr/testify/require" +) + +// TODO: TestGetClaimsHandler, TestGetIPNICIDHandler, TestGetDIDDocumentHandler, +// and the query/post-claim handler suites still need to be ported to the +// ucantone invocation flow. The legacy go-ucanto-based versions have been +// dropped during the migration. + +func TestGetRootHandler(t *testing.T) { + t.Run("success", func(t *testing.T) { + svr := httptest.NewServer(GetRootHandler(testutil.Service)) + defer svr.Close() + + res, err := http.Get(svr.URL) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + bytes, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.True(t, strings.Contains(string(bytes), testutil.Service.DID().String())) + }) + + t.Run("did:web", func(t *testing.T) { + didweb, err := did.Parse("did:web:example.org") + require.NoError(t, err) + + s := multikey.NewIssuer(didweb, testutil.Service) + + svr := httptest.NewServer(GetRootHandler(s)) + defer svr.Close() + + res, err := http.Get(svr.URL) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + bytes, err := io.ReadAll(res.Body) + require.NoError(t, err) + + body := string(bytes) + require.Contains(t, body, testutil.Service.Verifier().String()) + require.Contains(t, body, didweb.String()) + }) +} + +func TestGetClaimHandler(t *testing.T) { + store := contentclaims.NewStoreFromDatastore(datastore.NewMapDatastore()) + claim := testutil.RandomIndexClaim(t) + err := store.Put(context.Background(), claim.Link(), claim) + require.NoError(t, err) + + svr := httptest.NewServer(GetClaimHandler(store)) + defer svr.Close() + + t.Run("success", func(t *testing.T) { + res, err := http.Get(fmt.Sprintf("%s/claim/%s", svr.URL, claim.Link())) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + bytes, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Equal(t, claim.Bytes(), bytes) + }) + + t.Run("not found", func(t *testing.T) { + res, err := http.Get(fmt.Sprintf("%s/claim/%s", svr.URL, testutil.RandomCID(t))) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("invalid CID", func(t *testing.T) { + res, err := http.Get(fmt.Sprintf("%s/claim/invalid", svr.URL)) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) +} diff --git a/indexing-service/pkg/service/blobindexlookup/cachinglookup.go b/indexing-service/pkg/service/blobindexlookup/cachinglookup.go new file mode 100644 index 00000000..47619ab3 --- /dev/null +++ b/indexing-service/pkg/service/blobindexlookup/cachinglookup.go @@ -0,0 +1,64 @@ +package blobindexlookup + +import ( + "context" + "errors" + "fmt" + + "github.com/fil-forge/forge/indexing-service/pkg/service/providercacher" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/ipni/go-libipni/find/model" +) + +type cachingLookup struct { + blobIndexLookup BlobIndexLookup + shardDagIndexCache types.ShardedDagIndexStore + cachingQueue providercacher.CachingQueueQueuer +} + +var _ BlobIndexLookup = (*cachingLookup)(nil) + +// WithCache returns a blobIndexLookup that attempts to read blobs from the cache, and also caches providers asociated with index cids +func WithCache(blobIndexLookup BlobIndexLookup, shardedDagIndexCache types.ShardedDagIndexStore, cachingQueue providercacher.CachingQueueQueuer) BlobIndexLookup { + return &cachingLookup{ + blobIndexLookup: blobIndexLookup, + shardDagIndexCache: shardedDagIndexCache, + cachingQueue: cachingQueue, + } +} + +func (b *cachingLookup) Find(ctx context.Context, contextID types.EncodedContextID, provider model.ProviderResult, req types.RetrievalRequest) (blobindex.ShardedDagIndex, error) { + // attempt to read index from cache and return it if succesful + index, err := b.shardDagIndexCache.Get(ctx, contextID) + if err == nil { + return index, nil + } + + // if an error occurred other than the index not being in the cache, return it + if !errors.Is(err, types.ErrKeyNotFound) { + return nil, fmt.Errorf("reading from index cache: %w", err) + } + + // attempt to fetch the index from the underlying blob index lookup + index, err = b.blobIndexLookup.Find(ctx, contextID, provider, req) + if err != nil { + return nil, fmt.Errorf("fetching underlying index: %w", err) + } + + // cache the index for the future + if err := b.shardDagIndexCache.Set(ctx, contextID, index, true); err != nil { + return nil, fmt.Errorf("caching fetched index: %w", err) + } + + // queue a background cache of an provider record for all cids in the index without one + err = b.cachingQueue.Queue(ctx, providercacher.ProviderCachingJob{ + Provider: provider, + Index: index, + }) + if err != nil { + return nil, fmt.Errorf("queueing provider caching for index failed: %w", err) + } + + return index, nil +} diff --git a/indexing-service/pkg/service/blobindexlookup/cachinglookup_test.go b/indexing-service/pkg/service/blobindexlookup/cachinglookup_test.go new file mode 100644 index 00000000..79390078 --- /dev/null +++ b/indexing-service/pkg/service/blobindexlookup/cachinglookup_test.go @@ -0,0 +1,183 @@ +package blobindexlookup_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/service/blobindexlookup" + "github.com/fil-forge/forge/indexing-service/pkg/service/providercacher" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/ipni/go-libipni/find/model" + "github.com/stretchr/testify/require" +) + +func TestWithCache__Find(t *testing.T) { + cachedContextID := testutil.RandomBytes(t, 16) + notCachedContextID := testutil.RandomBytes(t, 16) + _, cachedIndex := testutil.RandomShardedDagIndex(t, 32) + _, notCachedIndex := testutil.RandomShardedDagIndex(t, 32) + + provider := testutil.RandomProviderResult(t) + + anError := errors.New("something went wrong") + testCases := []struct { + name string + contextID types.EncodedContextID + setErr error + getErr error + expectedErr error + baseLookup *mockBlobIndexLookup + providerCacher *mockCachingQueue + expectedIndex blobindex.ShardedDagIndex + finalState map[string]blobindex.ShardedDagIndex + }{ + { + name: "Index cached", + contextID: cachedContextID, + expectedIndex: cachedIndex, + finalState: map[string]blobindex.ShardedDagIndex{ + string(cachedContextID): cachedIndex, + }, + }, + { + name: "Index not cached, successful fetch", + contextID: notCachedContextID, + expectedIndex: notCachedIndex, + finalState: map[string]blobindex.ShardedDagIndex{ + string(cachedContextID): cachedIndex, + string(notCachedContextID): notCachedIndex, + }, + }, + { + name: "Lookup error", + contextID: cachedContextID, + expectedIndex: nil, + getErr: anError, + expectedErr: fmt.Errorf("reading from index cache: %w", anError), + finalState: map[string]blobindex.ShardedDagIndex{ + string(cachedContextID): cachedIndex, + }, + }, + { + name: "Save cache error", + contextID: notCachedContextID, + expectedIndex: nil, + setErr: anError, + expectedErr: fmt.Errorf("caching fetched index: %w", anError), + finalState: map[string]blobindex.ShardedDagIndex{ + string(cachedContextID): cachedIndex, + }, + }, + { + name: "underlying lookup error", + contextID: notCachedContextID, + expectedIndex: nil, + baseLookup: &mockBlobIndexLookup{nil, anError}, + expectedErr: fmt.Errorf("fetching underlying index: %w", anError), + finalState: map[string]blobindex.ShardedDagIndex{ + string(cachedContextID): cachedIndex, + }, + }, + { + name: "provider cacher error", + contextID: notCachedContextID, + expectedIndex: nil, + providerCacher: &mockCachingQueue{anError}, + expectedErr: fmt.Errorf("queueing provider caching for index failed: %w", anError), + finalState: map[string]blobindex.ShardedDagIndex{ + string(cachedContextID): cachedIndex, + string(notCachedContextID): notCachedIndex, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockStore := &MockShardedDagIndexStore{ + setErr: tc.setErr, + getErr: tc.getErr, + indexes: map[string]blobindex.ShardedDagIndex{ + string(cachedContextID): cachedIndex, + }, + } + lookup := tc.baseLookup + if lookup == nil { + lookup = &mockBlobIndexLookup{notCachedIndex, nil} + } + providerCacher := tc.providerCacher + if providerCacher == nil { + providerCacher = &mockCachingQueue{nil} + } + cl := blobindexlookup.WithCache(lookup, mockStore, providerCacher) + + req := types.RetrievalRequest{URL: *testutil.TestURL} + index, err := cl.Find(context.Background(), tc.contextID, provider, req) + if tc.expectedErr != nil { + require.EqualError(t, err, tc.expectedErr.Error()) + } else { + require.NoError(t, err) + } + testutil.RequireEqualIndex(t, tc.expectedIndex, index) + finalState := tc.finalState + if finalState == nil { + finalState = make(map[string]blobindex.ShardedDagIndex) + } + require.Equal(t, len(finalState), len(mockStore.indexes)) + for c, index := range mockStore.indexes { + expectedIndex := finalState[c] + testutil.RequireEqualIndex(t, expectedIndex, index) + } + }) + } +} + +type MockShardedDagIndexStore struct { + setErr, getErr error + indexes map[string]blobindex.ShardedDagIndex +} + +var _ types.ShardedDagIndexStore = &MockShardedDagIndexStore{} + +func (m *MockShardedDagIndexStore) SetExpirable(ctx context.Context, contextID types.EncodedContextID, expires bool) error { + return nil +} + +func (m *MockShardedDagIndexStore) Get(ctx context.Context, contextID types.EncodedContextID) (blobindex.ShardedDagIndex, error) { + if m.getErr != nil { + return nil, m.getErr + } + index, exists := m.indexes[string(contextID)] + if !exists { + return nil, types.ErrKeyNotFound + } + return index, nil +} + +func (m *MockShardedDagIndexStore) Set(ctx context.Context, contextID types.EncodedContextID, index blobindex.ShardedDagIndex, expire bool) error { + if m.setErr != nil { + return m.setErr + } + m.indexes[string(contextID)] = index + return nil +} + +type mockBlobIndexLookup struct { + index blobindex.ShardedDagIndex + err error +} + +func (m *mockBlobIndexLookup) Find(ctx context.Context, contextID types.EncodedContextID, provider model.ProviderResult, req types.RetrievalRequest) (blobindex.ShardedDagIndex, error) { + return m.index, m.err +} + +type mockCachingQueue struct { + err error +} + +func (m *mockCachingQueue) Queue(ctx context.Context, job providercacher.ProviderCachingJob) error { + return m.err +} diff --git a/indexing-service/pkg/service/blobindexlookup/interface.go b/indexing-service/pkg/service/blobindexlookup/interface.go new file mode 100644 index 00000000..5517e227 --- /dev/null +++ b/indexing-service/pkg/service/blobindexlookup/interface.go @@ -0,0 +1,20 @@ +package blobindexlookup + +import ( + "context" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/ipni/go-libipni/find/model" +) + +// BlobIndexLookup is a read through cache for fetching blob indexes +type BlobIndexLookup interface { + // Find should: + // 1. attempt to read the sharded dag index from the cache from the encoded contextID + // 2. if not found, attempt to fetch the index from the provided URL. Store the result in cache + // 3. return the index + // 4. asyncronously, add records to the ProviderStore from the parsed blob index so that we can avoid future queries to IPNI for + // other multihashes in the index + Find(ctx context.Context, contextID types.EncodedContextID, provider model.ProviderResult, req types.RetrievalRequest) (blobindex.ShardedDagIndex, error) +} diff --git a/indexing-service/pkg/service/blobindexlookup/mock_BlobIndexLookup.go b/indexing-service/pkg/service/blobindexlookup/mock_BlobIndexLookup.go new file mode 100644 index 00000000..df3c4914 --- /dev/null +++ b/indexing-service/pkg/service/blobindexlookup/mock_BlobIndexLookup.go @@ -0,0 +1,121 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package blobindexlookup + +import ( + "context" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/ipni/go-libipni/find/model" + mock "github.com/stretchr/testify/mock" +) + +// NewMockBlobIndexLookup creates a new instance of MockBlobIndexLookup. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockBlobIndexLookup(t interface { + mock.TestingT + Cleanup(func()) +}) *MockBlobIndexLookup { + mock := &MockBlobIndexLookup{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockBlobIndexLookup is an autogenerated mock type for the BlobIndexLookup type +type MockBlobIndexLookup struct { + mock.Mock +} + +type MockBlobIndexLookup_Expecter struct { + mock *mock.Mock +} + +func (_m *MockBlobIndexLookup) EXPECT() *MockBlobIndexLookup_Expecter { + return &MockBlobIndexLookup_Expecter{mock: &_m.Mock} +} + +// Find provides a mock function for the type MockBlobIndexLookup +func (_mock *MockBlobIndexLookup) Find(ctx context.Context, contextID types.EncodedContextID, provider model.ProviderResult, req types.RetrievalRequest) (blobindex.ShardedDagIndex, error) { + ret := _mock.Called(ctx, contextID, provider, req) + + if len(ret) == 0 { + panic("no return value specified for Find") + } + + var r0 blobindex.ShardedDagIndex + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, types.EncodedContextID, model.ProviderResult, types.RetrievalRequest) (blobindex.ShardedDagIndex, error)); ok { + return returnFunc(ctx, contextID, provider, req) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, types.EncodedContextID, model.ProviderResult, types.RetrievalRequest) blobindex.ShardedDagIndex); ok { + r0 = returnFunc(ctx, contextID, provider, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(blobindex.ShardedDagIndex) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, types.EncodedContextID, model.ProviderResult, types.RetrievalRequest) error); ok { + r1 = returnFunc(ctx, contextID, provider, req) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockBlobIndexLookup_Find_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Find' +type MockBlobIndexLookup_Find_Call struct { + *mock.Call +} + +// Find is a helper method to define mock.On call +// - ctx context.Context +// - contextID types.EncodedContextID +// - provider model.ProviderResult +// - req types.RetrievalRequest +func (_e *MockBlobIndexLookup_Expecter) Find(ctx interface{}, contextID interface{}, provider interface{}, req interface{}) *MockBlobIndexLookup_Find_Call { + return &MockBlobIndexLookup_Find_Call{Call: _e.mock.On("Find", ctx, contextID, provider, req)} +} + +func (_c *MockBlobIndexLookup_Find_Call) Run(run func(ctx context.Context, contextID types.EncodedContextID, provider model.ProviderResult, req types.RetrievalRequest)) *MockBlobIndexLookup_Find_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 types.EncodedContextID + if args[1] != nil { + arg1 = args[1].(types.EncodedContextID) + } + var arg2 model.ProviderResult + if args[2] != nil { + arg2 = args[2].(model.ProviderResult) + } + var arg3 types.RetrievalRequest + if args[3] != nil { + arg3 = args[3].(types.RetrievalRequest) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockBlobIndexLookup_Find_Call) Return(shardedDagIndex blobindex.ShardedDagIndex, err error) *MockBlobIndexLookup_Find_Call { + _c.Call.Return(shardedDagIndex, err) + return _c +} + +func (_c *MockBlobIndexLookup_Find_Call) RunAndReturn(run func(ctx context.Context, contextID types.EncodedContextID, provider model.ProviderResult, req types.RetrievalRequest) (blobindex.ShardedDagIndex, error)) *MockBlobIndexLookup_Find_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/service/blobindexlookup/simplelookup.go b/indexing-service/pkg/service/blobindexlookup/simplelookup.go new file mode 100644 index 00000000..37dc8cc3 --- /dev/null +++ b/indexing-service/pkg/service/blobindexlookup/simplelookup.go @@ -0,0 +1,149 @@ +package blobindexlookup + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/retrieval" + "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" + logging "github.com/ipfs/go-log/v2" + "github.com/ipni/go-libipni/find/model" +) + +var log = logging.Logger("blobindexlookup") + +type simpleLookup struct { + httpClient *http.Client +} + +var _ BlobIndexLookup = (*simpleLookup)(nil) + +func NewBlobIndexLookup(httpClient *http.Client) BlobIndexLookup { + return &simpleLookup{httpClient} +} + +// Find fetches the blob index from the given fetchURL +func (s *simpleLookup) Find(ctx context.Context, _ types.EncodedContextID, result model.ProviderResult, request types.RetrievalRequest) (blobindex.ShardedDagIndex, error) { + var body io.ReadCloser + // Make a UCAN authorized retrieval request. + body, err := doAuthorizedRetrieval(ctx, s.httpClient, request) + if err != nil { + return nil, fmt.Errorf("executing authorized retrieval: %w", err) + } + defer body.Close() + return blobindex.Extract(body) +} + +func doAuthorizedRetrieval(ctx context.Context, httpClient *http.Client, request types.RetrievalRequest) (io.ReadCloser, error) { + headers := http.Header{} + if request.Range != nil { + headers.Set("Range", fmt.Sprintf("bytes=%d-%d", request.Range.Start, request.Range.End)) + } + + log.Infow("authorized retrieval target", + "url", request.URL.String(), + "url_host", request.URL.Host, + "url_path", request.URL.Path, + "range", headers.Get("Range"), + "audience", request.Auth.Audience.String(), + "command", request.Auth.Command.String(), + "subject", request.Auth.Subject.String(), + "issuer", request.Auth.Issuer.DID().String(), + ) + + c, err := retrieval.NewClient( + &request.URL, + retrieval.WithHTTPClient(httpClient), + retrieval.WithHTTPHeaders(headers), + ) + if err != nil { + return nil, err + } + + prfLinks := make([]cid.Cid, 0, len(request.Auth.Proofs)) + for _, p := range request.Auth.Proofs { + prfLinks = append(prfLinks, p.Link()) + } + + inv, err := invocation.Invoke( + request.Auth.Issuer, + request.Auth.Subject, + request.Auth.Command, + request.Auth.Arguments, + invocation.WithProofs(prfLinks...), + invocation.WithAudience(request.Auth.Audience), + ) + if err != nil { + return nil, err + } + + res, err := c.Execute(execution.NewRequest(ctx, inv, execution.WithDelegations(request.Auth.Proofs...))) + if err != nil { + return nil, fmt.Errorf("executing retrieval invocation: %w", err) + } + + if log.LevelEnabled(logging.LevelDebug) { + // Diagnostic: dispatcher failures (InvalidAudience, HandlerNotFound, + // validation errors) are encoded as failure receipts with no metadata. + // The retrieval server then falls through to a default 200/empty/UCAN + // response, leaving the failure invisible at the HTTP layer. + if rcpt := res.Receipt(); rcpt != nil { + log.Debugw("retrieval receipt outcome", + "is_ok", rcpt.Out().IsOK(), + "is_err", rcpt.Out().IsErr(), + "ran", rcpt.Ran().String(), + ) + if rcpt.Out().IsErr() { + _, errBytes := rcpt.Out().Unpack() + log.Debugw("retrieval receipt failure", + "err_bytes_hex", fmt.Sprintf("%x", errBytes), + "err_bytes_str", string(errBytes), + ) + } + } + } + + meta, ok := res.Metadata().(*retrieval.HTTPHeaderResponseContainer) + if !ok { + return nil, errors.New("unexpected response metadata type") + } + if meta.StatusCode != http.StatusOK && meta.StatusCode != http.StatusPartialContent { + defer meta.Body.Close() + body, err := io.ReadAll(meta.Body) + if err != nil { + return nil, fmt.Errorf("reading response body: %w", err) + } + return nil, fmt.Errorf("retrieval failed with status code: %d, body: %s", meta.StatusCode, string(body)) + } + if log.LevelEnabled(logging.LevelDebug) { + // Diagnostic: confirm what we actually got back. Buffer the body + // so we can log a preview and still hand it to Extract. + all, readErr := io.ReadAll(meta.Body) + meta.Body.Close() + preview := all + if len(preview) > 64 { + preview = preview[:64] + } + headerDump := make(map[string][]string, len(meta.Header)) + for k, v := range meta.Header { + headerDump[k] = v + } + log.Debugw("authorized retrieval response", + "status", meta.StatusCode, + "body_len", len(all), + "body_preview_hex", fmt.Sprintf("%x", preview), + "read_err", readErr, + "headers", headerDump, + ) + return io.NopCloser(bytes.NewReader(all)), nil + } + return meta.Body, nil +} diff --git a/indexing-service/pkg/service/blobindexlookup/simplelookup_test.go b/indexing-service/pkg/service/blobindexlookup/simplelookup_test.go new file mode 100644 index 00000000..729bed05 --- /dev/null +++ b/indexing-service/pkg/service/blobindexlookup/simplelookup_test.go @@ -0,0 +1,82 @@ +package blobindexlookup_test + +import ( + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/service/blobindexlookup" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/commands/content" + "github.com/fil-forge/forge/protocol/retrieval" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/ipld/datamodel" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/command" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/stretchr/testify/require" +) + +var contentRetrieve = binding.Bind[*datamodel.Map, *datamodel.Map](command.MustParse("/content/retrieve")) + +func TestBlobIndexLookup__Find(t *testing.T) { + provider := testutil.RandomProviderResult(t) + _, index := testutil.RandomShardedDagIndex(t, 32) + + var indexBuf bytes.Buffer + require.NoError(t, blobindex.Archive(index, &indexBuf)) + indexBytes := indexBuf.Bytes() + indexEncodedLength := uint64(len(indexBytes)) + + srv := retrieval.NewServer(testutil.Service) + srv.Handle(contentRetrieve.Command, func(req execution.Request, res execution.Response) error { + respMeta := &retrieval.HTTPHeaderResponseContainer{ + Container: container.New(), + StatusCode: http.StatusOK, + Header: http.Header{ + "Content-Type": []string{"application/octet-stream"}, + }, + Body: io.NopCloser(bytes.NewReader(indexBytes)), + } + if err := res.SetMetadata(respMeta); err != nil { + return err + } + return res.SetSuccess(&datamodel.Map{}) + }) + + testServer := httptest.NewServer(srv) + t.Cleanup(testServer.Close) + + contentDigest := testutil.RandomMultihash(t) + + req := types.RetrievalRequest{ + URL: *testutil.Must(url.Parse(testServer.URL))(t), + Range: &content.Range{ + Start: 0, + End: indexEncodedLength - 1, + }, + Auth: types.RetrievalAuth{ + Issuer: testutil.Service, + Audience: testutil.Service.DID(), + Command: content.Retrieve.Command, + Subject: testutil.Service.DID(), + Arguments: &content.RetrieveArguments{ + Blob: content.Blob{Digest: contentDigest}, + Range: content.Range{Start: 0, End: indexEncodedLength - 1}, + }, + Proofs: []ucan.Delegation{}, + }, + } + + cl := blobindexlookup.NewBlobIndexLookup(testServer.Client()) + got, err := cl.Find(context.Background(), types.EncodedContextID(contentDigest), provider, req) + require.NoError(t, err) + testutil.RequireEqualIndex(t, index, got) +} diff --git a/indexing-service/pkg/service/contentclaims/cachingfinder.go b/indexing-service/pkg/service/contentclaims/cachingfinder.go new file mode 100644 index 00000000..27b98dc5 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/cachingfinder.go @@ -0,0 +1,51 @@ +package contentclaims + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" +) + +type cachingFinder struct { + finder Finder + cache types.ContentClaimsCache +} + +var _ Finder = (*cachingFinder)(nil) + +// WithCache augments a ClaimFinder with cached claims from a claim cache +func WithCache(finder Finder, cache types.ContentClaimsCache) Finder { + return &cachingFinder{finder, cache} +} + +// Find attempts to fetch a claim from either the local cache or via the provided URL (caching the result if its fetched) +func (cl *cachingFinder) Find(ctx context.Context, id cid.Cid, fetchURL *url.URL) (ucan.Invocation, error) { + // attempt to read claim from cache and return it if succesful + claim, err := cl.cache.Get(ctx, id) + if err == nil { + return claim, nil + } + + // if an error occurred other than the claim not being in the cache, return it + if !errors.Is(err, types.ErrKeyNotFound) { + return nil, fmt.Errorf("reading from claim cache: %w", err) + } + + // attempt to fetch the claim from the underlying claim finder + claim, err = cl.finder.Find(ctx, id, fetchURL) + if err != nil { + return nil, err + } + + // cache the claim for the future + err = cl.cache.Set(ctx, id, claim, true) + if err != nil { + return nil, fmt.Errorf("caching claim: %w", err) + } + return claim, nil +} diff --git a/indexing-service/pkg/service/contentclaims/cachingfinder_test.go b/indexing-service/pkg/service/contentclaims/cachingfinder_test.go new file mode 100644 index 00000000..c3f554e1 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/cachingfinder_test.go @@ -0,0 +1,163 @@ +package contentclaims_test + +import ( + "context" + "errors" + "fmt" + "net/url" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" +) + +func TestWithCache__Find(t *testing.T) { + cachedClaim := testutil.RandomLocationCommitment(t) + notCachedClaim := testutil.RandomIndexClaim(t) + + cachedCid := cachedClaim.Link() + notCachedCid := notCachedClaim.Link() + + anError := errors.New("something went wrong") + testCases := []struct { + name string + claimCid cid.Cid + setErr error + getErr error + expectedErr error + baseFinder *mockFinder + expectedClaim ucan.Invocation + finalState map[string]ucan.Invocation + }{ + { + name: "Claim cached", + claimCid: cachedCid, + expectedClaim: cachedClaim, + finalState: map[string]ucan.Invocation{ + cachedCid.String(): cachedClaim, + }, + }, + { + name: "Claim not cached, successful fetch", + claimCid: notCachedCid, + expectedClaim: notCachedClaim, + finalState: map[string]ucan.Invocation{ + cachedCid.String(): cachedClaim, + notCachedCid.String(): notCachedClaim, + }, + }, + { + name: "Find error", + claimCid: cachedCid, + expectedClaim: nil, + getErr: anError, + expectedErr: fmt.Errorf("reading from claim cache: %w", anError), + finalState: map[string]ucan.Invocation{ + cachedCid.String(): cachedClaim, + }, + }, + { + name: "Save cache error", + claimCid: notCachedCid, + expectedClaim: nil, + setErr: anError, + expectedErr: fmt.Errorf("caching claim: %w", anError), + finalState: map[string]ucan.Invocation{ + cachedCid.String(): cachedClaim, + }, + }, + { + name: "underlying find error", + claimCid: notCachedCid, + expectedClaim: nil, + baseFinder: &mockFinder{nil, anError}, + expectedErr: anError, + finalState: map[string]ucan.Invocation{ + cachedCid.String(): cachedClaim, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockCache := &MockContentClaimsCache{ + setErr: tc.setErr, + getErr: tc.getErr, + claims: map[string]ucan.Invocation{ + cachedCid.String(): cachedClaim, + }, + } + finder := tc.baseFinder + if finder == nil { + finder = &mockFinder{notCachedClaim, nil} + } + cl := contentclaims.WithCache(finder, mockCache) + + claim, err := cl.Find(context.Background(), tc.claimCid, testutil.TestURL) + if tc.expectedErr != nil { + require.EqualError(t, err, tc.expectedErr.Error()) + } else { + require.NoError(t, err) + } + testutil.RequireEqualClaim(t, tc.expectedClaim, claim) + finalState := tc.finalState + if finalState == nil { + finalState = make(map[string]ucan.Invocation) + } + require.Equal(t, len(finalState), len(mockCache.claims)) + for c, claim := range mockCache.claims { + expectedClaim := finalState[c] + testutil.RequireEqualClaim(t, expectedClaim, claim) + } + }) + } +} + +type MockContentClaimsCache struct { + setErr, getErr error + claims map[string]ucan.Invocation +} + +var _ types.ContentClaimsCache = &MockContentClaimsCache{} + +func (m *MockContentClaimsCache) SetExpirable(ctx context.Context, key cid.Cid, expires bool) error { + return nil +} + +func (m *MockContentClaimsCache) Get(ctx context.Context, claimCid cid.Cid) (ucan.Invocation, error) { + if m.getErr != nil { + return nil, m.getErr + } + claim, exists := m.claims[claimCid.String()] + if !exists { + return nil, types.ErrKeyNotFound + } + return claim, nil +} + +func (m *MockContentClaimsCache) Set(ctx context.Context, claimCid cid.Cid, claim ucan.Invocation, expires bool) error { + if m.setErr != nil { + return m.setErr + } + m.claims[claimCid.String()] = claim + return nil +} + +type mockFinder struct { + claim ucan.Invocation + err error +} + +func (m *mockFinder) Find(ctx context.Context, id cid.Cid, _ *url.URL) (ucan.Invocation, error) { + if m.err != nil { + return nil, m.err + } + if m.claim == nil || m.claim.Link() != id { + return nil, types.ErrKeyNotFound + } + return m.claim, nil +} diff --git a/indexing-service/pkg/service/contentclaims/errors.go b/indexing-service/pkg/service/contentclaims/errors.go new file mode 100644 index 00000000..dbef1efa --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/errors.go @@ -0,0 +1,5 @@ +package contentclaims + +import "github.com/fil-forge/ucantone/errors" + +var ErrMissingClaim = errors.New("MissingClaim", "Claim data was not found in the invocation payload.") diff --git a/indexing-service/pkg/service/contentclaims/identitycidfinder.go b/indexing-service/pkg/service/contentclaims/identitycidfinder.go new file mode 100644 index 00000000..152565d3 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/identitycidfinder.go @@ -0,0 +1,36 @@ +package contentclaims + +import ( + "context" + "net/url" + + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" +) + +type identityCidFinder struct { + finder Finder +} + +var _ Finder = (*identityCidFinder)(nil) + +// WithIdentityCids augments a ClaimFinder with claims retrieved automatically whenever an identity CID is used +func WithIdentityCids(finder Finder) Finder { + return &identityCidFinder{finder} +} + +// Find attempts to fetch a claim from either the permenant storage or via the provided URL +func (idf *identityCidFinder) Find(ctx context.Context, id cid.Cid, fetchURL *url.URL) (ucan.Invocation, error) { + if id.Prefix().MhType == multihash.IDENTITY { + dh, err := multihash.Decode(id.Hash()) + if err != nil { + return nil, err + } + return invocation.Decode(dh.Digest) + } + + // attempt to fetch the claim from the underlying claim finder + return idf.finder.Find(ctx, id, fetchURL) +} diff --git a/indexing-service/pkg/service/contentclaims/identitycidfinder_test.go b/indexing-service/pkg/service/contentclaims/identitycidfinder_test.go new file mode 100644 index 00000000..884be1fa --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/identitycidfinder_test.go @@ -0,0 +1,75 @@ +package contentclaims_test + +import ( + "context" + "errors" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + multihash "github.com/multiformats/go-multihash/core" + "github.com/stretchr/testify/require" +) + +func TestIdentityCidFinder__Find(t *testing.T) { + identityCidClaim := testutil.RandomLocationCommitment(t) + notIdentityCidClaim := testutil.RandomIndexClaim(t) + + identityCiddata := identityCidClaim.Bytes() + + identityCid := testutil.Must(cid.Prefix{ + Version: 1, + Codec: cid.Raw, + MhType: multihash.IDENTITY, + MhLength: len(identityCiddata), + }.Sum(identityCiddata))(t) + notIdentityCid := notIdentityCidClaim.Link() + + anError := errors.New("something went wrong") + testCases := []struct { + name string + claimCid cid.Cid + getErr error + expectedErr error + baseFinder *mockFinder + expectedClaim ucan.Invocation + }{ + { + name: "identity cid", + claimCid: identityCid, + expectedClaim: identityCidClaim, + }, + { + name: "not identity cid, successful fetch", + claimCid: notIdentityCid, + expectedClaim: notIdentityCidClaim, + }, + { + name: "underlying find error", + claimCid: notIdentityCid, + expectedClaim: nil, + baseFinder: &mockFinder{nil, anError}, + expectedErr: anError, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + finder := tc.baseFinder + if finder == nil { + finder = &mockFinder{notIdentityCidClaim, nil} + } + cl := contentclaims.WithIdentityCids(finder) + + claim, err := cl.Find(context.Background(), tc.claimCid, testutil.TestURL) + if tc.expectedErr != nil { + require.EqualError(t, err, tc.expectedErr.Error()) + } else { + require.NoError(t, err) + } + testutil.RequireEqualClaim(t, tc.expectedClaim, claim) + }) + } +} diff --git a/indexing-service/pkg/service/contentclaims/interface.go b/indexing-service/pkg/service/contentclaims/interface.go new file mode 100644 index 00000000..f7525f28 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/interface.go @@ -0,0 +1,27 @@ +package contentclaims + +import ( + "context" + "net/url" + + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" +) + +type Finder interface { + // Find and retrieve a claim via URL. + Find(ctx context.Context, claim cid.Cid, fetchURL *url.URL) (ucan.Invocation, error) +} + +type Service interface { + // Get reads the claim from the cache, or from storage if not found. + Get(ctx context.Context, claim cid.Cid) (ucan.Invocation, error) + // Find attempts to read the claim from the cache, falling back to retrieving + // it from storage and finally, if still not found, fetching it from the + // provided URL. The result is stored in the cache. + Find(ctx context.Context, claim cid.Cid, fetchURL *url.URL) (ucan.Invocation, error) + // Cache writes the claim to the cache with default expiry. + Cache(ctx context.Context, claim ucan.Invocation) error + // Publish writes the claim to the cache, and adds it to storage. + Publish(ctx context.Context, claim ucan.Invocation) error +} diff --git a/indexing-service/pkg/service/contentclaims/mock_ContentClaimsFinder.go b/indexing-service/pkg/service/contentclaims/mock_ContentClaimsFinder.go new file mode 100644 index 00000000..b02aedf7 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/mock_ContentClaimsFinder.go @@ -0,0 +1,115 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package contentclaims + +import ( + "context" + "net/url" + + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + mock "github.com/stretchr/testify/mock" +) + +// NewMockContentClaimsFinder creates a new instance of MockContentClaimsFinder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockContentClaimsFinder(t interface { + mock.TestingT + Cleanup(func()) +}) *MockContentClaimsFinder { + mock := &MockContentClaimsFinder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockContentClaimsFinder is an autogenerated mock type for the Finder type +type MockContentClaimsFinder struct { + mock.Mock +} + +type MockContentClaimsFinder_Expecter struct { + mock *mock.Mock +} + +func (_m *MockContentClaimsFinder) EXPECT() *MockContentClaimsFinder_Expecter { + return &MockContentClaimsFinder_Expecter{mock: &_m.Mock} +} + +// Find provides a mock function for the type MockContentClaimsFinder +func (_mock *MockContentClaimsFinder) Find(ctx context.Context, claim cid.Cid, fetchURL *url.URL) (ucan.Invocation, error) { + ret := _mock.Called(ctx, claim, fetchURL) + + if len(ret) == 0 { + panic("no return value specified for Find") + } + + var r0 ucan.Invocation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid, *url.URL) (ucan.Invocation, error)); ok { + return returnFunc(ctx, claim, fetchURL) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid, *url.URL) ucan.Invocation); ok { + r0 = returnFunc(ctx, claim, fetchURL) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ucan.Invocation) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, cid.Cid, *url.URL) error); ok { + r1 = returnFunc(ctx, claim, fetchURL) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockContentClaimsFinder_Find_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Find' +type MockContentClaimsFinder_Find_Call struct { + *mock.Call +} + +// Find is a helper method to define mock.On call +// - ctx context.Context +// - claim cid.Cid +// - fetchURL *url.URL +func (_e *MockContentClaimsFinder_Expecter) Find(ctx interface{}, claim interface{}, fetchURL interface{}) *MockContentClaimsFinder_Find_Call { + return &MockContentClaimsFinder_Find_Call{Call: _e.mock.On("Find", ctx, claim, fetchURL)} +} + +func (_c *MockContentClaimsFinder_Find_Call) Run(run func(ctx context.Context, claim cid.Cid, fetchURL *url.URL)) *MockContentClaimsFinder_Find_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 cid.Cid + if args[1] != nil { + arg1 = args[1].(cid.Cid) + } + var arg2 *url.URL + if args[2] != nil { + arg2 = args[2].(*url.URL) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockContentClaimsFinder_Find_Call) Return(invocation ucan.Invocation, err error) *MockContentClaimsFinder_Find_Call { + _c.Call.Return(invocation, err) + return _c +} + +func (_c *MockContentClaimsFinder_Find_Call) RunAndReturn(run func(ctx context.Context, claim cid.Cid, fetchURL *url.URL) (ucan.Invocation, error)) *MockContentClaimsFinder_Find_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/service/contentclaims/mock_ContentClaimsService.go b/indexing-service/pkg/service/contentclaims/mock_ContentClaimsService.go new file mode 100644 index 00000000..0c0fe373 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/mock_ContentClaimsService.go @@ -0,0 +1,297 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package contentclaims + +import ( + "context" + "net/url" + + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + mock "github.com/stretchr/testify/mock" +) + +// NewMockContentClaimsService creates a new instance of MockContentClaimsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockContentClaimsService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockContentClaimsService { + mock := &MockContentClaimsService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockContentClaimsService is an autogenerated mock type for the Service type +type MockContentClaimsService struct { + mock.Mock +} + +type MockContentClaimsService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockContentClaimsService) EXPECT() *MockContentClaimsService_Expecter { + return &MockContentClaimsService_Expecter{mock: &_m.Mock} +} + +// Cache provides a mock function for the type MockContentClaimsService +func (_mock *MockContentClaimsService) Cache(ctx context.Context, claim ucan.Invocation) error { + ret := _mock.Called(ctx, claim) + + if len(ret) == 0 { + panic("no return value specified for Cache") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ucan.Invocation) error); ok { + r0 = returnFunc(ctx, claim) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockContentClaimsService_Cache_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Cache' +type MockContentClaimsService_Cache_Call struct { + *mock.Call +} + +// Cache is a helper method to define mock.On call +// - ctx context.Context +// - claim ucan.Invocation +func (_e *MockContentClaimsService_Expecter) Cache(ctx interface{}, claim interface{}) *MockContentClaimsService_Cache_Call { + return &MockContentClaimsService_Cache_Call{Call: _e.mock.On("Cache", ctx, claim)} +} + +func (_c *MockContentClaimsService_Cache_Call) Run(run func(ctx context.Context, claim ucan.Invocation)) *MockContentClaimsService_Cache_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 ucan.Invocation + if args[1] != nil { + arg1 = args[1].(ucan.Invocation) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockContentClaimsService_Cache_Call) Return(err error) *MockContentClaimsService_Cache_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockContentClaimsService_Cache_Call) RunAndReturn(run func(ctx context.Context, claim ucan.Invocation) error) *MockContentClaimsService_Cache_Call { + _c.Call.Return(run) + return _c +} + +// Find provides a mock function for the type MockContentClaimsService +func (_mock *MockContentClaimsService) Find(ctx context.Context, claim cid.Cid, fetchURL *url.URL) (ucan.Invocation, error) { + ret := _mock.Called(ctx, claim, fetchURL) + + if len(ret) == 0 { + panic("no return value specified for Find") + } + + var r0 ucan.Invocation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid, *url.URL) (ucan.Invocation, error)); ok { + return returnFunc(ctx, claim, fetchURL) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid, *url.URL) ucan.Invocation); ok { + r0 = returnFunc(ctx, claim, fetchURL) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ucan.Invocation) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, cid.Cid, *url.URL) error); ok { + r1 = returnFunc(ctx, claim, fetchURL) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockContentClaimsService_Find_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Find' +type MockContentClaimsService_Find_Call struct { + *mock.Call +} + +// Find is a helper method to define mock.On call +// - ctx context.Context +// - claim cid.Cid +// - fetchURL *url.URL +func (_e *MockContentClaimsService_Expecter) Find(ctx interface{}, claim interface{}, fetchURL interface{}) *MockContentClaimsService_Find_Call { + return &MockContentClaimsService_Find_Call{Call: _e.mock.On("Find", ctx, claim, fetchURL)} +} + +func (_c *MockContentClaimsService_Find_Call) Run(run func(ctx context.Context, claim cid.Cid, fetchURL *url.URL)) *MockContentClaimsService_Find_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 cid.Cid + if args[1] != nil { + arg1 = args[1].(cid.Cid) + } + var arg2 *url.URL + if args[2] != nil { + arg2 = args[2].(*url.URL) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockContentClaimsService_Find_Call) Return(invocation ucan.Invocation, err error) *MockContentClaimsService_Find_Call { + _c.Call.Return(invocation, err) + return _c +} + +func (_c *MockContentClaimsService_Find_Call) RunAndReturn(run func(ctx context.Context, claim cid.Cid, fetchURL *url.URL) (ucan.Invocation, error)) *MockContentClaimsService_Find_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type MockContentClaimsService +func (_mock *MockContentClaimsService) Get(ctx context.Context, claim cid.Cid) (ucan.Invocation, error) { + ret := _mock.Called(ctx, claim) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 ucan.Invocation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid) (ucan.Invocation, error)); ok { + return returnFunc(ctx, claim) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid) ucan.Invocation); ok { + r0 = returnFunc(ctx, claim) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ucan.Invocation) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, cid.Cid) error); ok { + r1 = returnFunc(ctx, claim) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockContentClaimsService_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockContentClaimsService_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - claim cid.Cid +func (_e *MockContentClaimsService_Expecter) Get(ctx interface{}, claim interface{}) *MockContentClaimsService_Get_Call { + return &MockContentClaimsService_Get_Call{Call: _e.mock.On("Get", ctx, claim)} +} + +func (_c *MockContentClaimsService_Get_Call) Run(run func(ctx context.Context, claim cid.Cid)) *MockContentClaimsService_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 cid.Cid + if args[1] != nil { + arg1 = args[1].(cid.Cid) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockContentClaimsService_Get_Call) Return(invocation ucan.Invocation, err error) *MockContentClaimsService_Get_Call { + _c.Call.Return(invocation, err) + return _c +} + +func (_c *MockContentClaimsService_Get_Call) RunAndReturn(run func(ctx context.Context, claim cid.Cid) (ucan.Invocation, error)) *MockContentClaimsService_Get_Call { + _c.Call.Return(run) + return _c +} + +// Publish provides a mock function for the type MockContentClaimsService +func (_mock *MockContentClaimsService) Publish(ctx context.Context, claim ucan.Invocation) error { + ret := _mock.Called(ctx, claim) + + if len(ret) == 0 { + panic("no return value specified for Publish") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ucan.Invocation) error); ok { + r0 = returnFunc(ctx, claim) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockContentClaimsService_Publish_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Publish' +type MockContentClaimsService_Publish_Call struct { + *mock.Call +} + +// Publish is a helper method to define mock.On call +// - ctx context.Context +// - claim ucan.Invocation +func (_e *MockContentClaimsService_Expecter) Publish(ctx interface{}, claim interface{}) *MockContentClaimsService_Publish_Call { + return &MockContentClaimsService_Publish_Call{Call: _e.mock.On("Publish", ctx, claim)} +} + +func (_c *MockContentClaimsService_Publish_Call) Run(run func(ctx context.Context, claim ucan.Invocation)) *MockContentClaimsService_Publish_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 ucan.Invocation + if args[1] != nil { + arg1 = args[1].(ucan.Invocation) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockContentClaimsService_Publish_Call) Return(err error) *MockContentClaimsService_Publish_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockContentClaimsService_Publish_Call) RunAndReturn(run func(ctx context.Context, claim ucan.Invocation) error) *MockContentClaimsService_Publish_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/service/contentclaims/service.go b/indexing-service/pkg/service/contentclaims/service.go new file mode 100644 index 00000000..a9f327ff --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/service.go @@ -0,0 +1,59 @@ +package contentclaims + +import ( + "context" + "fmt" + "net/url" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" +) + +type ClaimService struct { + store types.ContentClaimsStore + cache types.ContentClaimsCache + finder Finder +} + +var _ Service = (*ClaimService)(nil) + +func (cs *ClaimService) Cache(ctx context.Context, claim ucan.Invocation) error { + return cs.cache.Set(ctx, claim.Link(), claim, true) +} + +func (cs *ClaimService) Find(ctx context.Context, claim cid.Cid, url *url.URL) (ucan.Invocation, error) { + return cs.finder.Find(ctx, claim, url) +} + +func (cs *ClaimService) Get(ctx context.Context, claim cid.Cid) (ucan.Invocation, error) { + c, err := cs.cache.Get(ctx, claim) + if err == nil { + return c, nil + } + if err != types.ErrKeyNotFound { + return nil, err + } + c, err = cs.store.Get(ctx, claim) + if err != nil { + return nil, fmt.Errorf("getting claim from store: %w", err) + } + err = cs.Cache(ctx, c) + if err != nil { + return nil, fmt.Errorf("caching claim: %w", err) + } + return c, nil +} + +func (cs *ClaimService) Publish(ctx context.Context, claim ucan.Invocation) error { + err := cs.store.Put(ctx, claim.Link(), claim) + if err != nil { + return fmt.Errorf("putting claim to store: %w", err) + } + return cs.Cache(ctx, claim) +} + +func New(store types.ContentClaimsStore, cache types.ContentClaimsCache, finder Finder) *ClaimService { + f := WithIdentityCids(WithCache(WithStore(finder, store), cache)) + return &ClaimService{store, cache, f} +} diff --git a/indexing-service/pkg/service/contentclaims/simplefinder.go b/indexing-service/pkg/service/contentclaims/simplefinder.go new file mode 100644 index 00000000..01a5f38e --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/simplefinder.go @@ -0,0 +1,50 @@ +package contentclaims + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" +) + +// simpleFinder is a read through cache for fetching content claims +type simpleFinder struct { + httpClient *http.Client +} + +var _ Finder = (*simpleFinder)(nil) + +// NewSimpleFinder creates a new [Finder] with the provided HTTP client. +func NewSimpleFinder(httpClient *http.Client) Finder { + return &simpleFinder{ + httpClient: httpClient, + } +} + +// Find attempts to fetch a claim from the provided URL. +func (sf *simpleFinder) Find(ctx context.Context, id cid.Cid, fetchURL *url.URL) (ucan.Invocation, error) { + // attempt to fetch the claim from provided url + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fetchURL.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := sf.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch claim: %w", err) + } + defer resp.Body.Close() + + var claim invocation.Invocation + if err := claim.UnmarshalCBOR(resp.Body); err != nil { + return nil, fmt.Errorf("failed to unmarshal claim: %w", err) + } + if id != claim.Link() { + return nil, fmt.Errorf("received claim: %s, does not match expected claim: %s", claim.Link(), id) + } + return &claim, nil +} diff --git a/indexing-service/pkg/service/contentclaims/simplefinder_test.go b/indexing-service/pkg/service/contentclaims/simplefinder_test.go new file mode 100644 index 00000000..4e339d92 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/simplefinder_test.go @@ -0,0 +1,63 @@ +package contentclaims_test + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/ucantone/ucan" + "github.com/stretchr/testify/require" +) + +func TestSimpleFinder__Find(t *testing.T) { + claim := testutil.RandomIndexClaim(t) + otherClaim := testutil.RandomIndexClaim(t) + + testCases := []struct { + name string + handler http.HandlerFunc + expectedErr error + expectedClaim ucan.Invocation + }{ + { + name: "success fetch", + handler: func(w http.ResponseWriter, r *http.Request) { + testutil.Must(w.Write(claim.Bytes()))(t) + }, + expectedClaim: claim, + }, + { + name: "CID match failure", + handler: func(w http.ResponseWriter, r *http.Request) { + testutil.Must(w.Write(otherClaim.Bytes()))(t) + }, + expectedErr: fmt.Errorf("received claim: %s, does not match expected claim: %s", otherClaim.Link(), claim.Link()), + }, + { + name: "failure", + handler: http.NotFound, + expectedErr: errors.New("failed to unmarshal claim: unmarshaling invocation envelope CBOR: cbor input should be of type array"), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testServer := httptest.NewServer(tc.handler) + defer func() { testServer.Close() }() + cl := contentclaims.NewSimpleFinder(testServer.Client()) + got, err := cl.Find(context.Background(), claim.Link(), testutil.Must(url.Parse(testServer.URL))(t)) + if tc.expectedErr != nil { + require.EqualError(t, err, strings.ReplaceAll(tc.expectedErr.Error(), "{url}", testServer.URL)) + } else { + require.NoError(t, err) + } + testutil.RequireEqualClaim(t, tc.expectedClaim, got) + }) + } +} diff --git a/indexing-service/pkg/service/contentclaims/store.go b/indexing-service/pkg/service/contentclaims/store.go new file mode 100644 index 00000000..3daeb9d6 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/store.go @@ -0,0 +1,86 @@ +package contentclaims + +import ( + "bytes" + "context" + "errors" + "fmt" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/go-ipni-tools/pkg/store" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" + "github.com/ipfs/go-datastore" +) + +type bucketStore struct { + bucket store.Store +} + +func (bs *bucketStore) Get(ctx context.Context, key cid.Cid) (ucan.Invocation, error) { + r, err := bs.bucket.Get(ctx, toKey(key)) + if err != nil { + if store.IsNotFound(err) { + return nil, types.ErrKeyNotFound + } + return nil, err + } + defer r.Close() + + var inv invocation.Invocation + if err := inv.UnmarshalCBOR(r); err != nil { + return nil, fmt.Errorf("failed to unmarshal invocation: %w", err) + } + return &inv, nil +} + +func (bs *bucketStore) Put(ctx context.Context, key cid.Cid, value ucan.Invocation) error { + b, err := invocation.Encode(value) + if err != nil { + return fmt.Errorf("failed to encode invocation: %w", err) + } + return bs.bucket.Put(ctx, toKey(key), uint64(len(b)), bytes.NewReader(b)) +} + +var _ types.ContentClaimsStore = (*bucketStore)(nil) + +// NewStoreFromBucket creates a claims store from a bucket style interface. +func NewStoreFromBucket(bucket store.Store) types.ContentClaimsStore { + return &bucketStore{bucket} +} + +type dsStore struct { + ds datastore.Datastore +} + +func (d *dsStore) Get(ctx context.Context, key cid.Cid) (ucan.Invocation, error) { + b, err := d.ds.Get(ctx, datastore.NewKey(toKey(key))) + if err != nil { + if errors.Is(err, datastore.ErrNotFound) { + return nil, types.ErrKeyNotFound + } + return nil, err + } + return invocation.Decode(b) + +} + +func (d *dsStore) Put(ctx context.Context, key cid.Cid, value ucan.Invocation) error { + b, err := invocation.Encode(value) + if err != nil { + return fmt.Errorf("failed to encode invocation: %w", err) + } + return d.ds.Put(ctx, datastore.NewKey(toKey(key)), b) +} + +var _ types.ContentClaimsStore = (*dsStore)(nil) + +func NewStoreFromDatastore(ds datastore.Datastore) types.ContentClaimsStore { + return &dsStore{ds} +} + +// toKey transforms the claim root CID into a string key. +func toKey(link cid.Cid) string { + return fmt.Sprintf("%s/%s", link, link) +} diff --git a/indexing-service/pkg/service/contentclaims/storefinder.go b/indexing-service/pkg/service/contentclaims/storefinder.go new file mode 100644 index 00000000..99db8ed8 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/storefinder.go @@ -0,0 +1,91 @@ +package contentclaims + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/ucantone/result" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" +) + +type storeFinder struct { + finder Finder + store types.ContentClaimsStore +} + +var _ Finder = (*storeFinder)(nil) + +// WithStore augments a ClaimFinder with claims retrieved from a claim store +func WithStore(finder Finder, store types.ContentClaimsStore) Finder { + return &storeFinder{finder, store} +} + +// Find attempts to fetch a claim from either the permenant storage or via the provided URL +func (sf *storeFinder) Find(ctx context.Context, id cid.Cid, fetchURL *url.URL) (ucan.Invocation, error) { + + // buffered channels so goroutines don't block. + storeCh := make(chan result.Result[ucan.Invocation, error], 1) + finderCh := make(chan result.Result[ucan.Invocation, error], 1) + + // Create a cancelable context for the store query. + storeCtx, cancelStore := context.WithCancel(ctx) + defer cancelStore() + + // Create a cancelable context for the finder query. + finderCtx, cancelFinder := context.WithCancel(ctx) + defer cancelFinder() + + // Start store query + go func() { + claim, err := sf.store.Get(storeCtx, id) + if err != nil { + storeCh <- result.Err[ucan.Invocation, error](err) + return + } + storeCh <- result.OK[ucan.Invocation, error](claim) + }() + + // Start finder query + go func() { + claim, err := sf.finder.Find(finderCtx, id, fetchURL) + if err != nil { + finderCh <- result.Err[ucan.Invocation, error](err) + return + } + finderCh <- result.OK[ucan.Invocation, error](claim) + }() + + var storeRes, finderRes result.Result[ucan.Invocation, error] + + // Wait for both responses. + for range 2 { + select { + case storeRes = <-storeCh: + if storeRes.IsOK() { + cancelFinder() + } + case finderRes = <-finderCh: + if finderRes.IsOK() { + cancelStore() + } + } + } + + if storeRes.IsOK() { + return storeRes.Unpack() + } + if finderRes.IsOK() { + return finderRes.Unpack() + } + _, storeErr := storeRes.Unpack() + _, finderErr := finderRes.Unpack() + // if an error occurred other than the claim not being in the store, return it + if !errors.Is(storeErr, types.ErrKeyNotFound) { + return nil, errors.Join(fmt.Errorf("reading from claim store: %w", storeErr), finderErr) + } + return nil, finderErr +} diff --git a/indexing-service/pkg/service/contentclaims/storefinder_test.go b/indexing-service/pkg/service/contentclaims/storefinder_test.go new file mode 100644 index 00000000..949d99b0 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/storefinder_test.go @@ -0,0 +1,108 @@ +package contentclaims_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" +) + +func TestWithStore__Find(t *testing.T) { + storedClaim := testutil.RandomLocationCommitment(t) + notStoredClaim := testutil.RandomIndexClaim(t) + + storedCid := storedClaim.Link() + notStoredCid := notStoredClaim.Link() + + anError := errors.New("something went wrong") + testCases := []struct { + name string + claimCid cid.Cid + getErr error + expectedErr error + baseFinder *mockFinder + expectedClaim ucan.Invocation + }{ + { + name: "Claim stored", + claimCid: storedCid, + expectedClaim: storedClaim, + }, + { + name: "Claim not stored, successful fetch", + claimCid: notStoredCid, + expectedClaim: notStoredClaim, + }, + { + name: "Find error", + claimCid: storedCid, + expectedClaim: nil, + getErr: anError, + expectedErr: errors.Join(fmt.Errorf("reading from claim store: %w", anError), types.ErrKeyNotFound), + }, + { + name: "underlying find error", + claimCid: notStoredCid, + expectedClaim: nil, + baseFinder: &mockFinder{nil, anError}, + expectedErr: anError, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockStore := &MockContentClaimsStore{ + getErr: tc.getErr, + claims: map[string]ucan.Invocation{ + storedCid.String(): storedClaim, + }, + } + finder := tc.baseFinder + if finder == nil { + finder = &mockFinder{notStoredClaim, nil} + } + cl := contentclaims.WithStore(finder, mockStore) + + claim, err := cl.Find(context.Background(), tc.claimCid, testutil.TestURL) + if tc.expectedErr != nil { + require.EqualError(t, err, tc.expectedErr.Error()) + } else { + require.NoError(t, err) + } + testutil.RequireEqualClaim(t, tc.expectedClaim, claim) + }) + } +} + +type MockContentClaimsStore struct { + setErr, getErr error + claims map[string]ucan.Invocation +} + +var _ types.ContentClaimsStore = &MockContentClaimsStore{} + +func (m *MockContentClaimsStore) Get(ctx context.Context, key cid.Cid) (ucan.Invocation, error) { + if m.getErr != nil { + return nil, m.getErr + } + claim, exists := m.claims[key.String()] + if !exists { + return nil, types.ErrKeyNotFound + } + return claim, nil +} + +func (m *MockContentClaimsStore) Put(ctx context.Context, key cid.Cid, claim ucan.Invocation) error { + if m.setErr != nil { + return m.setErr + } + m.claims[key.String()] = claim + return nil +} diff --git a/indexing-service/pkg/service/contentclaims/ucanserver.go b/indexing-service/pkg/service/contentclaims/ucanserver.go new file mode 100644 index 00000000..cfdfdbb4 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/ucanserver.go @@ -0,0 +1,16 @@ +package contentclaims + +import ( + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/ucan" +) + +func NewUCANServer(id ucan.Issuer, service types.Publisher, options ...server.HTTPOption) (*server.HTTPServer, error) { + ucanServer := server.NewHTTP(id, options...) + routes := NewUCANService(service) + for _, route := range routes { + ucanServer.Handle(route.Command, route.Handler) + } + return ucanServer, nil +} diff --git a/indexing-service/pkg/service/contentclaims/ucanserver_test.go b/indexing-service/pkg/service/contentclaims/ucanserver_test.go new file mode 100644 index 00000000..cedeb9ba --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/ucanserver_test.go @@ -0,0 +1,117 @@ +package contentclaims + +import ( + "context" + "net/http/httptest" + "net/url" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/commands" + assertcaps "github.com/fil-forge/forge/protocol/commands/assert" + claimcaps "github.com/fil-forge/forge/protocol/commands/claim" + "github.com/fil-forge/ucantone/client" + "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" +) + +func TestServer(t *testing.T) { + srv, err := NewUCANServer(testutil.Service, &mockIndexer{}) + require.NoError(t, err) + + httpSrv := httptest.NewServer(srv) + t.Cleanup(httpSrv.Close) + + srvURL, err := url.Parse(httpSrv.URL) + require.NoError(t, err) + + httpClient, err := client.NewHTTP(srvURL) + require.NoError(t, err) + + // Build a self-signed location commitment to attach to the cache invocation. + locationCommitment := testutil.Must(assertcaps.Location.Invoke( + testutil.Alice, + testutil.Alice.DID(), + &assertcaps.LocationArguments{ + Space: testutil.Bob.DID(), + Content: testutil.RandomMultihash(t), + Location: []commands.CborURL{commands.CborURL(*testutil.TestURL)}, + }, + ))(t) + + cacheInvocation := testutil.Must(claimcaps.Cache.Invoke( + testutil.Service, + testutil.Service.DID(), + &claimcaps.CacheArguments{ + Claim: locationCommitment.Link(), + Provider: claimcaps.Provider{ + Addresses: [][]byte{testutil.RandomMultiaddr(t).Bytes()}, + }, + }, + ))(t) + + invs := []struct { + name string + inv ucan.Invocation + opts []execution.RequestOption + }{ + { + name: assertcaps.Equals.String(), + inv: testutil.Must(assertcaps.Equals.Invoke( + testutil.Service, + testutil.Service.DID(), + &assertcaps.EqualsArguments{ + Content: testutil.RandomMultihash(t), + Equals: testutil.RandomCID(t), + }, + ))(t), + }, + { + name: assertcaps.Index.String(), + inv: testutil.Must(assertcaps.Index.Invoke( + testutil.Service, + testutil.Service.DID(), + &assertcaps.IndexArguments{ + Index: testutil.RandomCID(t), + }, + ))(t), + }, + { + name: claimcaps.Cache.String(), + inv: cacheInvocation, + opts: []execution.RequestOption{execution.WithInvocations(locationCommitment)}, + }, + } + + for _, tc := range invs { + t.Run(tc.name, func(t *testing.T) { + resp, err := httpClient.Execute(execution.NewRequest(t.Context(), tc.inv, tc.opts...)) + require.NoError(t, err) + require.False(t, resp.Receipt().Out().IsErr(), "unexpected failure") + }) + } +} + +type mockIndexer struct{} + +func (m *mockIndexer) Get(ctx context.Context, claim cid.Cid) (ucan.Invocation, error) { + return nil, nil +} + +func (m *mockIndexer) Cache(ctx context.Context, provider peer.AddrInfo, claim ucan.Invocation, meta ucan.Container) error { + return nil +} + +func (m *mockIndexer) Publish(ctx context.Context, claim ucan.Invocation, meta ucan.Container) error { + return nil +} + +func (m *mockIndexer) Query(ctx context.Context, q types.Query) (types.QueryResult, error) { + return nil, nil +} + +var _ types.Service = (*mockIndexer)(nil) diff --git a/indexing-service/pkg/service/contentclaims/ucanservice.go b/indexing-service/pkg/service/contentclaims/ucanservice.go new file mode 100644 index 00000000..a960a162 --- /dev/null +++ b/indexing-service/pkg/service/contentclaims/ucanservice.go @@ -0,0 +1,103 @@ +package contentclaims + +import ( + "github.com/fil-forge/forge/indexing-service/pkg/types" + assertcaps "github.com/fil-forge/forge/protocol/commands/assert" + claimcaps "github.com/fil-forge/forge/protocol/commands/claim" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/multikey/ed25519/verifier" + "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/ucan" + logging "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" +) + +var log = logging.Logger("contentclaims") + +func NewUCANService(service types.Publisher) []server.Route { + return []server.Route{ + assertcaps.Equals.Route( + func( + req *binding.Request[*assertcaps.EqualsArguments], + res *binding.Response[*assertcaps.EqualsOK], + ) error { + claim := req.Invocation() + err := service.Publish(req.Context(), claim, req.Metadata()) + if err != nil { + log.Errorf("publishing equals claim: %s", err) + return err + } + return res.SetSuccess(&assertcaps.EqualsOK{}) + }, + ), + assertcaps.Index.Route( + func( + req *binding.Request[*assertcaps.IndexArguments], + res *binding.Response[*assertcaps.IndexOK], + ) error { + claim := req.Invocation() + err := service.Publish(req.Context(), claim, req.Metadata()) + if err != nil { + log.Errorf("publishing index claim: %s", err) + return err + } + return res.SetSuccess(&assertcaps.IndexOK{}) + }, + ), + claimcaps.Cache.Route( + func( + req *binding.Request[*claimcaps.CacheArguments], + res *binding.Response[*claimcaps.CacheOK], + ) error { + args := req.Task().Arguments() + peerid, err := toPeerID(req.Invocation().Issuer()) + if err != nil { + return err + } + + var addrs []multiaddr.Multiaddr + for _, a := range args.Provider.Addresses { + ma, err := multiaddr.NewMultiaddrBytes(a) + if err != nil { + return err + } + addrs = append(addrs, ma) + } + + provider := peer.AddrInfo{ID: peerid, Addrs: addrs} + + var claim ucan.Invocation + for _, inv := range req.Metadata().Invocations() { + if inv.Link() == args.Claim { + claim = inv + break + } + } + if claim == nil { + return res.SetFailure(ErrMissingClaim) + } + + err = service.Cache(req.Context(), provider, claim, req.Metadata()) + if err != nil { + log.Errorf("caching claim: %s", err) + return err + } + return res.SetSuccess(&claimcaps.CacheOK{}) + }), + } +} + +func toPeerID(principal did.DID) (peer.ID, error) { + vfr, err := verifier.ParseKeyDID(principal.String()) + if err != nil { + return "", err + } + pub, err := crypto.UnmarshalEd25519PublicKey(vfr.Raw()) + if err != nil { + return "", err + } + return peer.IDFromPublicKey(pub) +} diff --git a/indexing-service/pkg/service/providercacher/cachingqueue.go b/indexing-service/pkg/service/providercacher/cachingqueue.go new file mode 100644 index 00000000..6e7038c4 --- /dev/null +++ b/indexing-service/pkg/service/providercacher/cachingqueue.go @@ -0,0 +1,33 @@ +package providercacher + +import ( + "context" + + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/go-ipni-tools/pkg/queue" + "github.com/ipni/go-libipni/find/model" +) + +type ( + CachingQueueQueuer = queue.QueueQueuer[ProviderCachingJob] + CachingQueue = queue.Queue[ProviderCachingJob] + + ProviderCachingJob struct { + Provider model.ProviderResult + Index blobindex.ShardedDagIndex + } + + JobHandler struct { + providerCacher ProviderCacher + } +) + +func NewJobHandler(providerCacher ProviderCacher) *JobHandler { + return &JobHandler{ + providerCacher: providerCacher, + } +} + +func (j *JobHandler) Handle(ctx context.Context, job ProviderCachingJob) error { + return j.providerCacher.CacheProviderForIndexRecords(ctx, job.Provider, job.Index) +} diff --git a/indexing-service/pkg/service/providercacher/cachingqueuepoller.go b/indexing-service/pkg/service/providercacher/cachingqueuepoller.go new file mode 100644 index 00000000..a45d57e3 --- /dev/null +++ b/indexing-service/pkg/service/providercacher/cachingqueuepoller.go @@ -0,0 +1,14 @@ +package providercacher + +import ( + "github.com/fil-forge/go-ipni-tools/pkg/queue" +) + +// CachingQueuePoller polls a queue for provider caching jobs and processes them +// using the provided ProviderCacher and SQSCachingDecoder. +type CachingQueuePoller = queue.QueuePoller[ProviderCachingJob] + +// NewCachingQueuePoller creates a new CachingQueuePoller instance. +func NewCachingQueuePoller(cachingQueue CachingQueue, cacher ProviderCacher, opts ...queue.Option) (*CachingQueuePoller, error) { + return queue.NewQueuePoller(cachingQueue, queue.JobHandler(NewJobHandler(cacher).Handle), opts...) +} diff --git a/indexing-service/pkg/service/providercacher/cachingqueuepoller_test.go b/indexing-service/pkg/service/providercacher/cachingqueuepoller_test.go new file mode 100644 index 00000000..48e6ebee --- /dev/null +++ b/indexing-service/pkg/service/providercacher/cachingqueuepoller_test.go @@ -0,0 +1,172 @@ +package providercacher_test + +import ( + "context" + "errors" + "sync" + "testing" + "time" + + "github.com/fil-forge/forge/indexing-service/pkg/service/providercacher" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/go-ipni-tools/pkg/queue" + "github.com/ipni/go-libipni/find/model" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestCachingQueuePoller_StartStop(t *testing.T) { + mockQueue := providercacher.NewMockCachingQueue(t) + mockCacher := providercacher.NewMockProviderCacher(t) + + mockQueue.EXPECT().Read(mock.Anything, mock.Anything).Return([]queue.WithID[providercacher.ProviderCachingJob]{}, nil) + + poller, err := providercacher.NewCachingQueuePoller(mockQueue, mockCacher) + require.NoError(t, err) + + poller.Start() + time.Sleep(20 * time.Millisecond) + poller.Stop() +} + +func TestCachingQueuePoller_BatchProcessing(t *testing.T) { + const ( + numJobs = 11 + batchSize = 2 + fullBatches = numJobs / batchSize + lastBatchSize = numJobs % batchSize + ) + + testJob := queue.WithID[providercacher.ProviderCachingJob]{ + ID: "test-job", + Job: providercacher.ProviderCachingJob{ + Provider: model.ProviderResult{Provider: &peer.AddrInfo{ID: peer.ID("test-peer")}}, + Index: blobindex.NewShardedDagIndex(0), + }, + } + + mockQueue := providercacher.NewMockCachingQueue(t) + mockCacher := providercacher.NewMockProviderCacher(t) + + batch := make([]queue.WithID[providercacher.ProviderCachingJob], batchSize) + for i := range batchSize { + batch[i] = testJob + } + mockQueue.EXPECT().Read(mock.Anything, batchSize).Return(batch, nil).Times(fullBatches) + mockQueue.EXPECT().Read(mock.Anything, batchSize).Return(batch[:lastBatchSize], nil).Once() + mockQueue.EXPECT().Read(mock.Anything, batchSize).Return([]queue.WithID[providercacher.ProviderCachingJob]{}, nil). + Run(func(ctx context.Context, _ int) { + <-ctx.Done() + }). + Return([]queue.WithID[providercacher.ProviderCachingJob]{}, nil). + Once() + + var wg sync.WaitGroup + wg.Add(numJobs) + + mockCacher.EXPECT(). + CacheProviderForIndexRecords(mock.Anything, testJob.Job.Provider, testJob.Job.Index). + Run(func(ctx context.Context, _ model.ProviderResult, _ blobindex.ShardedDagIndex) { + defer wg.Done() + }). + Return(nil). + Times(numJobs) + + mockQueue.EXPECT(). + Delete(mock.Anything, testJob.ID). + Return(nil). + Times(numJobs) + + poller, err := providercacher.NewCachingQueuePoller( + mockQueue, + mockCacher, + queue.WithJobBatchSize(batchSize), + ) + require.NoError(t, err) + + poller.Start() + wg.Wait() + poller.Stop() +} + +func TestCachingQueuePoller_FailedJobsAreRetried(t *testing.T) { + successfulJob := queue.WithID[providercacher.ProviderCachingJob]{ + ID: "successful-job", + Job: providercacher.ProviderCachingJob{ + Provider: model.ProviderResult{Provider: &peer.AddrInfo{ID: peer.ID("successful-peer")}}, + Index: blobindex.NewShardedDagIndex(0), + }, + } + + failedJob := queue.WithID[providercacher.ProviderCachingJob]{ + ID: "failed-job", + Job: providercacher.ProviderCachingJob{ + Provider: model.ProviderResult{Provider: &peer.AddrInfo{ID: peer.ID("failed-peer")}}, + Index: blobindex.NewShardedDagIndex(0), + }, + } + + mockQueue := providercacher.NewMockCachingQueue(t) + mockCacher := providercacher.NewMockProviderCacher(t) + + mockQueue.EXPECT().Read(mock.Anything, mock.Anything).Return( + []queue.WithID[providercacher.ProviderCachingJob]{successfulJob, failedJob}, nil, + ).Once() + mockQueue.EXPECT().Read(mock.Anything, mock.Anything).Return([]queue.WithID[providercacher.ProviderCachingJob]{}, nil). + Run(func(ctx context.Context, _ int) { + <-ctx.Done() + }). + Return([]queue.WithID[providercacher.ProviderCachingJob]{}, nil). + Once() + + mockCacher.EXPECT().CacheProviderForIndexRecords(mock.Anything, successfulJob.Job.Provider, successfulJob.Job.Index). + Return(nil).Once() + mockCacher.EXPECT().CacheProviderForIndexRecords(mock.Anything, failedJob.Job.Provider, failedJob.Job.Index). + Return(errors.New("processing error")).Once() + + mockQueue.EXPECT().Delete(mock.Anything, successfulJob.ID).Return(nil).Once() + mockQueue.EXPECT().Release(mock.Anything, failedJob.ID).Return(nil).Once() + + poller, err := providercacher.NewCachingQueuePoller(mockQueue, mockCacher) + require.NoError(t, err) + + poller.Start() + time.Sleep(50 * time.Millisecond) + poller.Stop() +} + +func TestCachingQueuePoller_JobsTimingOutAreNotRetried(t *testing.T) { + bigJob := queue.WithID[providercacher.ProviderCachingJob]{ + ID: "big-job", + Job: providercacher.ProviderCachingJob{ + Provider: model.ProviderResult{Provider: &peer.AddrInfo{ID: peer.ID("peer")}}, + Index: blobindex.NewShardedDagIndex(0), + }, + } + + mockQueue := providercacher.NewMockCachingQueue(t) + mockCacher := providercacher.NewMockProviderCacher(t) + + mockQueue.EXPECT().Read(mock.Anything, mock.Anything).Return( + []queue.WithID[providercacher.ProviderCachingJob]{bigJob}, nil, + ).Once() + mockQueue.EXPECT().Read(mock.Anything, mock.Anything).Return([]queue.WithID[providercacher.ProviderCachingJob]{}, nil). + Run(func(ctx context.Context, _ int) { + <-ctx.Done() + }). + Return([]queue.WithID[providercacher.ProviderCachingJob]{}, nil). + Once() + + mockCacher.EXPECT().CacheProviderForIndexRecords(mock.Anything, bigJob.Job.Provider, bigJob.Job.Index). + Return(context.DeadlineExceeded).Once() + + mockQueue.EXPECT().Delete(mock.Anything, bigJob.ID).Return(nil).Once() + + poller, err := providercacher.NewCachingQueuePoller(mockQueue, mockCacher) + require.NoError(t, err) + + poller.Start() + time.Sleep(50 * time.Millisecond) + poller.Stop() +} diff --git a/indexing-service/pkg/service/providercacher/interface.go b/indexing-service/pkg/service/providercacher/interface.go new file mode 100644 index 00000000..bc5e9911 --- /dev/null +++ b/indexing-service/pkg/service/providercacher/interface.go @@ -0,0 +1,12 @@ +package providercacher + +import ( + "context" + + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/ipni/go-libipni/find/model" +) + +type ProviderCacher interface { + CacheProviderForIndexRecords(ctx context.Context, provider model.ProviderResult, index blobindex.ShardedDagIndex) error +} diff --git a/indexing-service/pkg/service/providercacher/mock_CachingQueue.go b/indexing-service/pkg/service/providercacher/mock_CachingQueue.go new file mode 100644 index 00000000..4902ce31 --- /dev/null +++ b/indexing-service/pkg/service/providercacher/mock_CachingQueue.go @@ -0,0 +1,278 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package providercacher + +import ( + "context" + + "github.com/fil-forge/go-ipni-tools/pkg/queue" + mock "github.com/stretchr/testify/mock" +) + +// NewMockCachingQueue creates a new instance of MockCachingQueue. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockCachingQueue(t interface { + mock.TestingT + Cleanup(func()) +}) *MockCachingQueue { + mock := &MockCachingQueue{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockCachingQueue is an autogenerated mock type for the CachingQueue type +type MockCachingQueue struct { + mock.Mock +} + +type MockCachingQueue_Expecter struct { + mock *mock.Mock +} + +func (_m *MockCachingQueue) EXPECT() *MockCachingQueue_Expecter { + return &MockCachingQueue_Expecter{mock: &_m.Mock} +} + +// Delete provides a mock function for the type MockCachingQueue +func (_mock *MockCachingQueue) Delete(ctx context.Context, jobID string) error { + ret := _mock.Called(ctx, jobID) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = returnFunc(ctx, jobID) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCachingQueue_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockCachingQueue_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - jobID string +func (_e *MockCachingQueue_Expecter) Delete(ctx interface{}, jobID interface{}) *MockCachingQueue_Delete_Call { + return &MockCachingQueue_Delete_Call{Call: _e.mock.On("Delete", ctx, jobID)} +} + +func (_c *MockCachingQueue_Delete_Call) Run(run func(ctx context.Context, jobID string)) *MockCachingQueue_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockCachingQueue_Delete_Call) Return(err error) *MockCachingQueue_Delete_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCachingQueue_Delete_Call) RunAndReturn(run func(ctx context.Context, jobID string) error) *MockCachingQueue_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Queue provides a mock function for the type MockCachingQueue +func (_mock *MockCachingQueue) Queue(ctx context.Context, job ProviderCachingJob) error { + ret := _mock.Called(ctx, job) + + if len(ret) == 0 { + panic("no return value specified for Queue") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ProviderCachingJob) error); ok { + r0 = returnFunc(ctx, job) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCachingQueue_Queue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Queue' +type MockCachingQueue_Queue_Call struct { + *mock.Call +} + +// Queue is a helper method to define mock.On call +// - ctx context.Context +// - job ProviderCachingJob +func (_e *MockCachingQueue_Expecter) Queue(ctx interface{}, job interface{}) *MockCachingQueue_Queue_Call { + return &MockCachingQueue_Queue_Call{Call: _e.mock.On("Queue", ctx, job)} +} + +func (_c *MockCachingQueue_Queue_Call) Run(run func(ctx context.Context, job ProviderCachingJob)) *MockCachingQueue_Queue_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 ProviderCachingJob + if args[1] != nil { + arg1 = args[1].(ProviderCachingJob) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockCachingQueue_Queue_Call) Return(err error) *MockCachingQueue_Queue_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCachingQueue_Queue_Call) RunAndReturn(run func(ctx context.Context, job ProviderCachingJob) error) *MockCachingQueue_Queue_Call { + _c.Call.Return(run) + return _c +} + +// Read provides a mock function for the type MockCachingQueue +func (_mock *MockCachingQueue) Read(ctx context.Context, maxJobs int) ([]queue.WithID[ProviderCachingJob], error) { + ret := _mock.Called(ctx, maxJobs) + + if len(ret) == 0 { + panic("no return value specified for Read") + } + + var r0 []queue.WithID[ProviderCachingJob] + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, int) ([]queue.WithID[ProviderCachingJob], error)); ok { + return returnFunc(ctx, maxJobs) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, int) []queue.WithID[ProviderCachingJob]); ok { + r0 = returnFunc(ctx, maxJobs) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]queue.WithID[ProviderCachingJob]) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = returnFunc(ctx, maxJobs) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockCachingQueue_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read' +type MockCachingQueue_Read_Call struct { + *mock.Call +} + +// Read is a helper method to define mock.On call +// - ctx context.Context +// - maxJobs int +func (_e *MockCachingQueue_Expecter) Read(ctx interface{}, maxJobs interface{}) *MockCachingQueue_Read_Call { + return &MockCachingQueue_Read_Call{Call: _e.mock.On("Read", ctx, maxJobs)} +} + +func (_c *MockCachingQueue_Read_Call) Run(run func(ctx context.Context, maxJobs int)) *MockCachingQueue_Read_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 int + if args[1] != nil { + arg1 = args[1].(int) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockCachingQueue_Read_Call) Return(withIDs []queue.WithID[ProviderCachingJob], err error) *MockCachingQueue_Read_Call { + _c.Call.Return(withIDs, err) + return _c +} + +func (_c *MockCachingQueue_Read_Call) RunAndReturn(run func(ctx context.Context, maxJobs int) ([]queue.WithID[ProviderCachingJob], error)) *MockCachingQueue_Read_Call { + _c.Call.Return(run) + return _c +} + +// Release provides a mock function for the type MockCachingQueue +func (_mock *MockCachingQueue) Release(ctx context.Context, jobID string) error { + ret := _mock.Called(ctx, jobID) + + if len(ret) == 0 { + panic("no return value specified for Release") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = returnFunc(ctx, jobID) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCachingQueue_Release_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Release' +type MockCachingQueue_Release_Call struct { + *mock.Call +} + +// Release is a helper method to define mock.On call +// - ctx context.Context +// - jobID string +func (_e *MockCachingQueue_Expecter) Release(ctx interface{}, jobID interface{}) *MockCachingQueue_Release_Call { + return &MockCachingQueue_Release_Call{Call: _e.mock.On("Release", ctx, jobID)} +} + +func (_c *MockCachingQueue_Release_Call) Run(run func(ctx context.Context, jobID string)) *MockCachingQueue_Release_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockCachingQueue_Release_Call) Return(err error) *MockCachingQueue_Release_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCachingQueue_Release_Call) RunAndReturn(run func(ctx context.Context, jobID string) error) *MockCachingQueue_Release_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/service/providercacher/mock_ProviderCacher.go b/indexing-service/pkg/service/providercacher/mock_ProviderCacher.go new file mode 100644 index 00000000..c2bf4dc8 --- /dev/null +++ b/indexing-service/pkg/service/providercacher/mock_ProviderCacher.go @@ -0,0 +1,103 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package providercacher + +import ( + "context" + + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/ipni/go-libipni/find/model" + mock "github.com/stretchr/testify/mock" +) + +// NewMockProviderCacher creates a new instance of MockProviderCacher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockProviderCacher(t interface { + mock.TestingT + Cleanup(func()) +}) *MockProviderCacher { + mock := &MockProviderCacher{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockProviderCacher is an autogenerated mock type for the ProviderCacher type +type MockProviderCacher struct { + mock.Mock +} + +type MockProviderCacher_Expecter struct { + mock *mock.Mock +} + +func (_m *MockProviderCacher) EXPECT() *MockProviderCacher_Expecter { + return &MockProviderCacher_Expecter{mock: &_m.Mock} +} + +// CacheProviderForIndexRecords provides a mock function for the type MockProviderCacher +func (_mock *MockProviderCacher) CacheProviderForIndexRecords(ctx context.Context, provider model.ProviderResult, index blobindex.ShardedDagIndex) error { + ret := _mock.Called(ctx, provider, index) + + if len(ret) == 0 { + panic("no return value specified for CacheProviderForIndexRecords") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, model.ProviderResult, blobindex.ShardedDagIndex) error); ok { + r0 = returnFunc(ctx, provider, index) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockProviderCacher_CacheProviderForIndexRecords_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CacheProviderForIndexRecords' +type MockProviderCacher_CacheProviderForIndexRecords_Call struct { + *mock.Call +} + +// CacheProviderForIndexRecords is a helper method to define mock.On call +// - ctx context.Context +// - provider model.ProviderResult +// - index blobindex.ShardedDagIndex +func (_e *MockProviderCacher_Expecter) CacheProviderForIndexRecords(ctx interface{}, provider interface{}, index interface{}) *MockProviderCacher_CacheProviderForIndexRecords_Call { + return &MockProviderCacher_CacheProviderForIndexRecords_Call{Call: _e.mock.On("CacheProviderForIndexRecords", ctx, provider, index)} +} + +func (_c *MockProviderCacher_CacheProviderForIndexRecords_Call) Run(run func(ctx context.Context, provider model.ProviderResult, index blobindex.ShardedDagIndex)) *MockProviderCacher_CacheProviderForIndexRecords_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 model.ProviderResult + if args[1] != nil { + arg1 = args[1].(model.ProviderResult) + } + var arg2 blobindex.ShardedDagIndex + if args[2] != nil { + arg2 = args[2].(blobindex.ShardedDagIndex) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockProviderCacher_CacheProviderForIndexRecords_Call) Return(err error) *MockProviderCacher_CacheProviderForIndexRecords_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockProviderCacher_CacheProviderForIndexRecords_Call) RunAndReturn(run func(ctx context.Context, provider model.ProviderResult, index blobindex.ShardedDagIndex) error) *MockProviderCacher_CacheProviderForIndexRecords_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/service/providercacher/simpleprovidercacher.go b/indexing-service/pkg/service/providercacher/simpleprovidercacher.go new file mode 100644 index 00000000..e4ea17ef --- /dev/null +++ b/indexing-service/pkg/service/providercacher/simpleprovidercacher.go @@ -0,0 +1,61 @@ +package providercacher + +import ( + "context" + "fmt" + + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/ipni/go-libipni/find/model" + "go.opentelemetry.io/otel/attribute" +) + +// MaxBatchSize is the maximum number of items that'll be added to a batch. +const MaxBatchSize = 10_000 + +type simpleProviderCacher struct { + providerStore types.ProviderStore +} + +func NewSimpleProviderCacher(providerStore types.ProviderStore) ProviderCacher { + return &simpleProviderCacher{providerStore: providerStore} +} + +func (s *simpleProviderCacher) CacheProviderForIndexRecords(ctx context.Context, provider model.ProviderResult, index blobindex.ShardedDagIndex) error { + ctx, span := telemetry.StartSpan(ctx, "ProviderCacher.CacheProviderForIndexRecords") + defer span.End() + + batch := s.providerStore.Batch() + total := 0 + size := 1 + for _, shardIndex := range index.Shards().Iterator() { + for hash := range shardIndex.Iterator() { + err := batch.Add(ctx, hash, provider) + if err != nil { + return fmt.Errorf("batch adding provider: %w", err) + } + err = batch.SetExpirable(ctx, hash, true) + if err != nil { + return fmt.Errorf("batch setting provider expirable: %w", err) + } + total++ + size++ + if size >= MaxBatchSize { + span.AddEvent("commit batch") + err := batch.Commit(ctx) + if err != nil { + return fmt.Errorf("batch commiting: %w", err) + } + batch = s.providerStore.Batch() + size = 0 + } + } + } + span.SetAttributes(attribute.KeyValue{Key: "total", Value: attribute.IntValue(total)}) + if size == 0 { + return nil + } + span.AddEvent("commit batch") + return batch.Commit(ctx) +} diff --git a/indexing-service/pkg/service/providercacher/simpleprovidercacher_test.go b/indexing-service/pkg/service/providercacher/simpleprovidercacher_test.go new file mode 100644 index 00000000..fc88abcc --- /dev/null +++ b/indexing-service/pkg/service/providercacher/simpleprovidercacher_test.go @@ -0,0 +1,249 @@ +package providercacher_test + +import ( + "context" + "slices" + "sync" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/service/providercacher" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/ipni/go-libipni/find/model" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +func TestSimpleProviderCacher_CacheProviderForIndexRecords(t *testing.T) { + ctx := context.Background() + + testProvider := testutil.RandomProviderResult(t) + testProvider2 := testutil.RandomProviderResult(t) + + shardIndex := blobindex.NewShardedDagIndex(2) + + shardMhs := testutil.RandomMultihashes(t, 2) + sliceMhs := testutil.RandomMultihashes(t, 6) + for i := range 2 { + for j := range 3 { + shardIndex.SetSlice(shardMhs[i], sliceMhs[i*3+j], blobindex.Range{}) + } + } + rootMh := testutil.RandomMultihash(t) + shardIndex.SetSlice(shardMhs[0], rootMh, blobindex.Range{}) + + shardIndex2 := blobindex.NewShardedDagIndex(2) + for j := range 2 { + shardIndex2.SetSlice(shardMhs[0], sliceMhs[j], blobindex.Range{}) + } + rootMh2 := testutil.RandomMultihash(t) + shardIndex2.SetSlice(shardMhs[0], rootMh2, blobindex.Range{}) + + evensFilled := func() map[string][]model.ProviderResult { + starter := make(map[string][]model.ProviderResult) + for i, sliceMh := range sliceMhs { + if i%2 == 0 { + starter[sliceMh.String()] = []model.ProviderResult{testProvider} + } + } + return starter + } + + testCases := []struct { + name string + provider model.ProviderResult + index blobindex.ShardedDagIndex + getErr error + setErr error + initialStore map[string][]model.ProviderResult + expectedErr error + testStore func(t *testing.T, store map[string][]model.ProviderResult) + }{ + { + name: "Cache new provider", + provider: testProvider, + index: shardIndex, + expectedErr: nil, + testStore: func(t *testing.T, store map[string][]model.ProviderResult) { + require.Len(t, store, 7) + for _, sliceMh := range sliceMhs { + require.Equal(t, store[sliceMh.String()], []model.ProviderResult{testProvider}) + } + }, + }, + { + name: "Cache provider already present", + provider: testProvider, + index: shardIndex, + initialStore: evensFilled(), + expectedErr: nil, + testStore: func(t *testing.T, store map[string][]model.ProviderResult) { + require.Len(t, store, 7) + for _, sliceMh := range sliceMhs { + require.Equal(t, store[sliceMh.String()], []model.ProviderResult{testProvider}) + } + }, + }, + { + name: "Cache another provider on top", + provider: testProvider2, + index: shardIndex, + initialStore: evensFilled(), + expectedErr: nil, + testStore: func(t *testing.T, store map[string][]model.ProviderResult) { + require.Len(t, store, 7) + for i, sliceMh := range sliceMhs { + expected := []model.ProviderResult{testProvider2} + if i%2 == 0 { + expected = []model.ProviderResult{testProvider, testProvider2} + } + require.Equal(t, store[sliceMh.String()], expected) + } + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + initialStore := tc.initialStore + if initialStore == nil { + initialStore = make(map[string][]model.ProviderResult) + } + mockStore := &MockProviderStore{ + setErr: tc.setErr, + getErr: tc.getErr, + store: initialStore, + } + + cacher := providercacher.NewSimpleProviderCacher(mockStore) + + err := cacher.CacheProviderForIndexRecords(ctx, tc.provider, tc.index) + if tc.expectedErr == nil { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr.Error()) + } + if tc.testStore != nil { + tc.testStore(t, mockStore.store) + } + }) + } +} + +// Simulate a 10k NFT - a directory with 10k image/metadata files +func TestSimpleProviderCacher_10kNFT(t *testing.T) { + ctx := context.Background() + + prov := testutil.RandomProviderResult(t) + idx := blobindex.NewShardedDagIndex(2) + + shardDigests := testutil.RandomMultihashes(t, 2) + sliceDigests := make([]multihash.Multihash, 0, 10_000) + for range 10_000 { + for { + digest := testutil.RandomMultihash(t) + if slices.ContainsFunc(sliceDigests, func(s multihash.Multihash) bool { return s.String() == digest.String() }) { + continue + } + sliceDigests = append(sliceDigests, digest) + break + } + } + + for i := range 2 { + for j := range 5_000 { + idx.SetSlice(shardDigests[i], sliceDigests[i*5_000+j], blobindex.Range{}) + } + } + rootMh := testutil.RandomMultihash(t) + idx.SetSlice(shardDigests[0], rootMh, blobindex.Range{}) + + mockStore := &MockProviderStore{store: map[string][]model.ProviderResult{}} + + cacher := providercacher.NewSimpleProviderCacher(mockStore) + err := cacher.CacheProviderForIndexRecords(ctx, prov, idx) + require.NoError(t, err) +} + +type MockProviderStore struct { + setErr, getErr error + store map[string][]model.ProviderResult + mutex sync.RWMutex +} + +var _ types.ProviderStore = &MockProviderStore{} + +func (m *MockProviderStore) Members(ctx context.Context, hash multihash.Multihash) ([]model.ProviderResult, error) { + if m.getErr != nil { + return nil, m.getErr + } + m.mutex.RLock() + defer m.mutex.RUnlock() + results, exists := m.store[hash.String()] + if !exists { + return nil, types.ErrKeyNotFound + } + return results, nil +} + +func (m *MockProviderStore) Add(ctx context.Context, hash multihash.Multihash, providers ...model.ProviderResult) (uint64, error) { + written := uint64(0) + m.mutex.Lock() + defer m.mutex.Unlock() + for _, provider := range providers { + existing := m.store[hash.String()] + if !slices.ContainsFunc(existing, func(p model.ProviderResult) bool { return p.Equal(provider) }) { + m.store[hash.String()] = append(existing, provider) + written++ + } + } + return written, nil +} + +func (m *MockProviderStore) SetExpirable(ctx context.Context, key multihash.Multihash, expires bool) error { + return nil +} + +func (m *MockProviderStore) Batch() types.ValueSetCacheBatcher[multihash.Multihash, model.ProviderResult] { + return &MockBatcher{store: m} +} + +type MockCommand struct { + op string + key multihash.Multihash + values []model.ProviderResult + expire bool +} + +type MockBatcher struct { + store *MockProviderStore + commands []MockCommand +} + +func (mb *MockBatcher) Add(ctx context.Context, key multihash.Multihash, newProviders ...model.ProviderResult) error { + mb.commands = append(mb.commands, MockCommand{op: "add", key: key, values: newProviders}) + return nil +} + +func (mb *MockBatcher) SetExpirable(ctx context.Context, key multihash.Multihash, expires bool) error { + mb.commands = append(mb.commands, MockCommand{op: "setExpirable", key: key, expire: expires}) + return nil +} + +func (mb *MockBatcher) Commit(ctx context.Context) error { + for _, c := range mb.commands { + if c.op == "add" { + _, err := mb.store.Add(ctx, c.key, c.values...) + if err != nil { + return err + } + } else if c.op == "setExpirable" { + err := mb.store.SetExpirable(ctx, c.key, c.expire) + if err != nil { + return err + } + } + } + return nil +} diff --git a/indexing-service/pkg/service/providerindex/interface.go b/indexing-service/pkg/service/providerindex/interface.go new file mode 100644 index 00000000..285d4586 --- /dev/null +++ b/indexing-service/pkg/service/providerindex/interface.go @@ -0,0 +1,30 @@ +package providerindex + +import ( + "context" + "iter" + + "github.com/ipni/go-libipni/find/model" + meta "github.com/ipni/go-libipni/metadata" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multihash" +) + +// ProviderIndex is a read/write interface to a local cache of providers that falls back to legacy systems and IPNI +type ProviderIndex interface { + // Find should do the following + // 1. Read from the IPNI Storage cache to get a list of providers + // a. If there is no record in cache, query IPNI, filter out any non-content claims metadata, and store + // the resulting records in the cache + // b. the are no records in the cache or IPNI, it can attempt to read from legacy systems -- Dynamo tables & content claims storage, synthetically constructing provider results + // 2. With returned provider results, filter additionally for claim type. If space dids are set, calculate an encodedcontextid's by hashing space DID and Hash, and filter for a matching context id + // Future TODO: kick off a conversion task to update the recrds + Find(context.Context, QueryKey) ([]model.ProviderResult, error) + // Cache writes entries to the cache but does not publish/announce an + // advertisement for them. Entries expire after a pre-determined time. + Cache(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta meta.Metadata) error + // Publish should do the following: + // 1. Write the entries to the cache with no expiration until publishing is complete + // 2. Generate an advertisement for the advertised hashes and publish/announce it + Publish(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta meta.Metadata) error +} diff --git a/indexing-service/pkg/service/providerindex/mock_ProviderIndex.go b/indexing-service/pkg/service/providerindex/mock_ProviderIndex.go new file mode 100644 index 00000000..1bb86939 --- /dev/null +++ b/indexing-service/pkg/service/providerindex/mock_ProviderIndex.go @@ -0,0 +1,261 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package providerindex + +import ( + "context" + "iter" + + "github.com/ipni/go-libipni/find/model" + "github.com/ipni/go-libipni/metadata" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multihash" + mock "github.com/stretchr/testify/mock" +) + +// NewMockProviderIndex creates a new instance of MockProviderIndex. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockProviderIndex(t interface { + mock.TestingT + Cleanup(func()) +}) *MockProviderIndex { + mock := &MockProviderIndex{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockProviderIndex is an autogenerated mock type for the ProviderIndex type +type MockProviderIndex struct { + mock.Mock +} + +type MockProviderIndex_Expecter struct { + mock *mock.Mock +} + +func (_m *MockProviderIndex) EXPECT() *MockProviderIndex_Expecter { + return &MockProviderIndex_Expecter{mock: &_m.Mock} +} + +// Cache provides a mock function for the type MockProviderIndex +func (_mock *MockProviderIndex) Cache(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta metadata.Metadata) error { + ret := _mock.Called(ctx, provider, contextID, digests, meta) + + if len(ret) == 0 { + panic("no return value specified for Cache") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, peer.AddrInfo, string, iter.Seq[multihash.Multihash], metadata.Metadata) error); ok { + r0 = returnFunc(ctx, provider, contextID, digests, meta) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockProviderIndex_Cache_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Cache' +type MockProviderIndex_Cache_Call struct { + *mock.Call +} + +// Cache is a helper method to define mock.On call +// - ctx context.Context +// - provider peer.AddrInfo +// - contextID string +// - digests iter.Seq[multihash.Multihash] +// - meta metadata.Metadata +func (_e *MockProviderIndex_Expecter) Cache(ctx interface{}, provider interface{}, contextID interface{}, digests interface{}, meta interface{}) *MockProviderIndex_Cache_Call { + return &MockProviderIndex_Cache_Call{Call: _e.mock.On("Cache", ctx, provider, contextID, digests, meta)} +} + +func (_c *MockProviderIndex_Cache_Call) Run(run func(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta metadata.Metadata)) *MockProviderIndex_Cache_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 peer.AddrInfo + if args[1] != nil { + arg1 = args[1].(peer.AddrInfo) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 iter.Seq[multihash.Multihash] + if args[3] != nil { + arg3 = args[3].(iter.Seq[multihash.Multihash]) + } + var arg4 metadata.Metadata + if args[4] != nil { + arg4 = args[4].(metadata.Metadata) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) + }) + return _c +} + +func (_c *MockProviderIndex_Cache_Call) Return(err error) *MockProviderIndex_Cache_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockProviderIndex_Cache_Call) RunAndReturn(run func(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta metadata.Metadata) error) *MockProviderIndex_Cache_Call { + _c.Call.Return(run) + return _c +} + +// Find provides a mock function for the type MockProviderIndex +func (_mock *MockProviderIndex) Find(context1 context.Context, queryKey QueryKey) ([]model.ProviderResult, error) { + ret := _mock.Called(context1, queryKey) + + if len(ret) == 0 { + panic("no return value specified for Find") + } + + var r0 []model.ProviderResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, QueryKey) ([]model.ProviderResult, error)); ok { + return returnFunc(context1, queryKey) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, QueryKey) []model.ProviderResult); ok { + r0 = returnFunc(context1, queryKey) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]model.ProviderResult) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, QueryKey) error); ok { + r1 = returnFunc(context1, queryKey) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockProviderIndex_Find_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Find' +type MockProviderIndex_Find_Call struct { + *mock.Call +} + +// Find is a helper method to define mock.On call +// - context1 context.Context +// - queryKey QueryKey +func (_e *MockProviderIndex_Expecter) Find(context1 interface{}, queryKey interface{}) *MockProviderIndex_Find_Call { + return &MockProviderIndex_Find_Call{Call: _e.mock.On("Find", context1, queryKey)} +} + +func (_c *MockProviderIndex_Find_Call) Run(run func(context1 context.Context, queryKey QueryKey)) *MockProviderIndex_Find_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 QueryKey + if args[1] != nil { + arg1 = args[1].(QueryKey) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockProviderIndex_Find_Call) Return(providerResults []model.ProviderResult, err error) *MockProviderIndex_Find_Call { + _c.Call.Return(providerResults, err) + return _c +} + +func (_c *MockProviderIndex_Find_Call) RunAndReturn(run func(context1 context.Context, queryKey QueryKey) ([]model.ProviderResult, error)) *MockProviderIndex_Find_Call { + _c.Call.Return(run) + return _c +} + +// Publish provides a mock function for the type MockProviderIndex +func (_mock *MockProviderIndex) Publish(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta metadata.Metadata) error { + ret := _mock.Called(ctx, provider, contextID, digests, meta) + + if len(ret) == 0 { + panic("no return value specified for Publish") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, peer.AddrInfo, string, iter.Seq[multihash.Multihash], metadata.Metadata) error); ok { + r0 = returnFunc(ctx, provider, contextID, digests, meta) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockProviderIndex_Publish_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Publish' +type MockProviderIndex_Publish_Call struct { + *mock.Call +} + +// Publish is a helper method to define mock.On call +// - ctx context.Context +// - provider peer.AddrInfo +// - contextID string +// - digests iter.Seq[multihash.Multihash] +// - meta metadata.Metadata +func (_e *MockProviderIndex_Expecter) Publish(ctx interface{}, provider interface{}, contextID interface{}, digests interface{}, meta interface{}) *MockProviderIndex_Publish_Call { + return &MockProviderIndex_Publish_Call{Call: _e.mock.On("Publish", ctx, provider, contextID, digests, meta)} +} + +func (_c *MockProviderIndex_Publish_Call) Run(run func(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta metadata.Metadata)) *MockProviderIndex_Publish_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 peer.AddrInfo + if args[1] != nil { + arg1 = args[1].(peer.AddrInfo) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 iter.Seq[multihash.Multihash] + if args[3] != nil { + arg3 = args[3].(iter.Seq[multihash.Multihash]) + } + var arg4 metadata.Metadata + if args[4] != nil { + arg4 = args[4].(metadata.Metadata) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) + }) + return _c +} + +func (_c *MockProviderIndex_Publish_Call) Return(err error) *MockProviderIndex_Publish_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockProviderIndex_Publish_Call) RunAndReturn(run func(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[multihash.Multihash], meta metadata.Metadata) error) *MockProviderIndex_Publish_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/service/providerindex/providerindex.go b/indexing-service/pkg/service/providerindex/providerindex.go new file mode 100644 index 00000000..f62c6d54 --- /dev/null +++ b/indexing-service/pkg/service/providerindex/providerindex.go @@ -0,0 +1,401 @@ +package providerindex + +import ( + "bytes" + "context" + "errors" + "fmt" + "iter" + "slices" + "sync" + "time" + + "github.com/benbjohnson/clock" + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/digestutil" + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + "github.com/fil-forge/go-ipni-tools/pkg/publisher" + "github.com/fil-forge/ucantone/did" + logging "github.com/ipfs/go-log/v2" + ipnifind "github.com/ipni/go-libipni/find/client" + "github.com/ipni/go-libipni/find/model" + meta "github.com/ipni/go-libipni/metadata" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multicodec" + mh "github.com/multiformats/go-multihash" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +const ( + // MaxBatchSize is the maximum number of items that'll be added to a batch. + MaxBatchSize = 10_000 + // IPNITimeout is the duration after which an IPNI query will be cancelled. + // It is set relatively high, since the IPNI client is tiered, with individual + // timeouts set at 1.5s. + IPNITimeout = 5 * time.Second +) + +type QueryKey struct { + Spaces []did.DID + Hash mh.Multihash + TargetClaims []multicodec.Code +} + +// ProviderIndexService is a read/write interface to a local cache of providers that falls back to IPNI +type ProviderIndexService struct { + providerStore types.ProviderStore + noProviderStore types.NoProviderStore + findClient ipnifind.Finder + asyncPublisher publisher.AsyncPublisher + mutex sync.Mutex + clock clock.Clock + log logging.EventLogger +} + +var _ ProviderIndex = (*ProviderIndexService)(nil) + +type config struct { + log logging.EventLogger + clock clock.Clock +} + +// Option configures an ProviderIndex. +type Option func(conf *config) + +// WithLogger configures the service to use the passed logger instead of the +// default logger. +func WithLogger(log logging.EventLogger) Option { + return func(conf *config) { + conf.log = log + } +} + +// WithClock configures the provider index with a mockable clock for testing. +func WithClock(clock clock.Clock) Option { + return func(conf *config) { + conf.clock = clock + } +} + +func New(providerStore types.ProviderStore, noProviderStore types.NoProviderStore, findClient ipnifind.Finder, asyncPublisher publisher.AsyncPublisher, options ...Option) *ProviderIndexService { + conf := config{} + for _, option := range options { + option(&conf) + } + if conf.clock == nil { + conf.clock = clock.New() + } + if conf.log == nil { + conf.log = logging.Logger("providerindex") + } + return &ProviderIndexService{ + providerStore: providerStore, + noProviderStore: noProviderStore, + findClient: findClient, + asyncPublisher: asyncPublisher, + clock: conf.clock, + log: conf.log, + } +} + +// Find should do the following +// 1. Read from the IPNI Storage cache to get a list of providers +// a. if there are no records in the cache, query IPNI, filtering out any non-content claims metadata +// b. if no records are found in IPNI, attempt to read claims from legacy systems -- Dynamo tables & content +// claims storage, synthetically constructing provider results +// c. finally, store the resulting records in the cache +// 2. With returned provider results, filter additionally for claim type. If space dids are set, calculate an +// encodedcontextid's by hashing space DID and Hash, and filter for a matching context id +// Future TODO: kick off a conversion task to update the records +func (pi *ProviderIndexService) Find(ctx context.Context, qk QueryKey) ([]model.ProviderResult, error) { + ctx, s := telemetry.StartSpan(ctx, "ProviderIndexService.Find") + defer s.End() + + s.AddEvent("finding ProviderResults") + results, err := pi.getProviderResults(ctx, qk.Hash, qk.TargetClaims) + if err != nil { + telemetry.Error(s, err, "finding ProviderResults") + return nil, fmt.Errorf("getting provider results: %w", err) + } + + s.AddEvent("filtering results by space") + return filterBySpace(results, qk.Hash, qk.Spaces) +} + +func (pi *ProviderIndexService) getProviderResults(ctx context.Context, mh mh.Multihash, targetClaims []multicodec.Code) ([]model.ProviderResult, error) { + ctx, s := telemetry.StartSpan(ctx, "ProviderIndexService.getProviderResults") + defer s.End() + + s.AddEvent("searching in cache") + res, err := pi.providerStore.Members(ctx, mh) + if err == nil { + res, _ = filterCodecs(res, targetClaims) + if len(res) > 0 { + s.AddEvent("cache hit") + return res, nil + } + } else { + if !errors.Is(err, types.ErrKeyNotFound) { + telemetry.Error(s, err, "fetching from cache") + return nil, err + } + } + + s.AddEvent("fetching from IPNI") + results, err := pi.fetchFromIPNI(ctx, s, mh, targetClaims) + s.AddEvent("fetched from IPNI", trace.WithAttributes(attribute.Bool("found", len(results) != 0))) + if err != nil { + return nil, fmt.Errorf("fetching from IPNI failed: %w", err) + } + if len(results) > 0 { + pi.cacheResults(ctx, s, mh, results) + } + return results, nil +} + +// Helper function to cache results. +func (pi *ProviderIndexService) cacheResults(ctx context.Context, s trace.Span, mh mh.Multihash, results []model.ProviderResult) { + s.AddEvent("caching results") + n, err := pi.providerStore.Add(ctx, mh, results...) + if err != nil { + telemetry.Error(s, err, "caching results") + pi.log.Errorf("adding results to set: %s", err) + return + } + if n > 0 { + if err := pi.providerStore.SetExpirable(ctx, mh, true); err != nil { + telemetry.Error(s, err, "setting cache entry expiration") + pi.log.Errorf("setting expirable: %s", err) + } + } +} + +// Helper function to cache empty results. +func (pi *ProviderIndexService) cacheNoProviderResults(ctx context.Context, s trace.Span, mh mh.Multihash, targetClaims []multicodec.Code) { + s.AddEvent("caching no results") + n, err := pi.noProviderStore.Add(ctx, mh, targetClaims...) + if err != nil { + telemetry.Error(s, err, "caching no provider results") + pi.log.Errorf("caching no results: %s", err) + return + } + + if n > 0 { + if err := pi.noProviderStore.SetExpirable(ctx, mh, true); err != nil { + telemetry.Error(s, err, "setting no provider results expiration") + pi.log.Errorf("setting no results expirable: %s", err) + } + } +} + +func (pi *ProviderIndexService) fetchFromIPNI(ctx context.Context, s trace.Span, mh mh.Multihash, targetClaims []multicodec.Code) ([]model.ProviderResult, error) { + var results []model.ProviderResult + + // check if we already know there are no results in IPNI + codes, err := pi.noProviderStore.Members(ctx, mh) + if err == nil { + missingClaims, _ := filter(targetClaims, func(targetCode multicodec.Code) (bool, error) { + return !slices.Contains(codes, targetCode), nil + }) + if len(missingClaims) == 0 { + return nil, nil + } + } else { + if !errors.Is(err, types.ErrKeyNotFound) { + telemetry.Error(s, err, "fetching from cache") + return nil, err + } + } + + // IPNI will occassionally hang. If it does, don't wait for it. + ctx, cancel := pi.clock.WithTimeout(ctx, IPNITimeout) + defer cancel() + + findRes, err := pi.findClient.Find(ctx, mh) + if err != nil { + pi.log.Warnf("finding %s in IPNI: %s", digestutil.Format(mh), err) + } else { + for _, mhres := range findRes.MultihashResults { + results = append(results, mhres.ProviderResults...) + } + + results, err = filterCodecs(results, targetClaims) + if err != nil { + return nil, fmt.Errorf("filtering codecs: %w", err) + } + } + if len(results) == 0 { + pi.cacheNoProviderResults(ctx, s, mh, targetClaims) + } + return results, nil +} + +func filterCodecs(results []model.ProviderResult, codecs []multicodec.Code) ([]model.ProviderResult, error) { + if len(codecs) == 0 { + return results, nil + } + return filter(results, func(result model.ProviderResult) (bool, error) { + md := metadata.MetadataContext.New() + err := md.UnmarshalBinary(result.Metadata) + if err != nil { + return false, err + } + return slices.ContainsFunc(codecs, func(code multicodec.Code) bool { + return slices.ContainsFunc(md.Protocols(), func(mdCode multicodec.Code) bool { + return mdCode == code + }) + }), nil + }) +} + +func filterBySpace(results []model.ProviderResult, mh mh.Multihash, spaces []did.DID) ([]model.ProviderResult, error) { + if len(spaces) == 0 { + return results, nil + } + encryptedIds := make([]types.EncodedContextID, 0, len(spaces)) + for _, space := range spaces { + encryptedId, err := types.ContextID{ + Space: &space, + Hash: mh, + }.ToEncoded() + if err != nil { + return nil, err + } + encryptedIds = append(encryptedIds, encryptedId) + } + + filtered, err := filter(results, func(result model.ProviderResult) (bool, error) { + if !filterableByContextID(result) { + return true, nil + } + return slices.ContainsFunc(encryptedIds, func(encyptedID types.EncodedContextID) bool { + return bytes.Equal(result.ContextID, encyptedID) + }), nil + }) + if err != nil { + return nil, err + } + return filtered, nil +} + +func (pi *ProviderIndexService) Cache(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[mh.Multihash], meta meta.Metadata) error { + // Cache the entries _with_ expiry - we cannot rely on the IPNI notifier to + // tell us when they are published since we are not publishing to IPNI. + return Cache(ctx, pi.log, pi.providerStore, provider, contextID, digests, meta, true) +} + +func Cache(ctx context.Context, log logging.EventLogger, providerStore types.ProviderStore, provider peer.AddrInfo, contextID string, digests iter.Seq[mh.Multihash], meta meta.Metadata, expire bool) error { + log.Infof("caching provider results for context: %s, provider: %s", contextID, provider.ID) + ctx, s := telemetry.StartSpan(ctx, "ProviderIndexService.Cache") + defer s.End() + + mdb, err := meta.MarshalBinary() + if err != nil { + return fmt.Errorf("marshaling metadata: %w", err) + } + + pr := model.ProviderResult{ + ContextID: []byte(contextID), + Metadata: mdb, + Provider: &provider, + } + + batch := providerStore.Batch() + size := 0 + total := 0 + for d := range digests { + err := batch.Add(ctx, d, pr) + if err != nil { + return err + } + err = batch.SetExpirable(ctx, d, expire) + if err != nil { + return err + } + total++ + + size++ + if size >= MaxBatchSize { + s.AddEvent("commit batch") + err := batch.Commit(ctx) + if err != nil { + return fmt.Errorf("comitting batch: %w", err) + } + batch = providerStore.Batch() + size = 0 + } + } + + if size > 0 { + s.AddEvent("commit batch") + err = batch.Commit(ctx) + if err != nil { + return fmt.Errorf("comitting batch: %w", err) + } + } + + s.SetAttributes(attribute.KeyValue{Key: "total", Value: attribute.IntValue(total)}) + log.Infof("cached %d provider results for context: %s", total, contextID) + return nil +} + +// Publish should do the following: +// 1. Write the entries to the cache with no expiration until publishing is complete +// 2. Generate an advertisement for the advertised hashes and publish/announce it +func (pi *ProviderIndexService) Publish(ctx context.Context, provider peer.AddrInfo, contextID string, digests iter.Seq[mh.Multihash], meta meta.Metadata) error { + ctx, s := telemetry.StartSpan(ctx, "ProviderIndexService.Publish") + defer s.End() + + // cache but do not expire (entries will be expired via the notifier) + s.AddEvent("start pre-cache") + err := Cache(ctx, pi.log, pi.providerStore, provider, contextID, digests, meta, false) + if err != nil { + return fmt.Errorf("caching provider results: %w", err) + } + + pi.mutex.Lock() + defer pi.mutex.Unlock() + + s.AddEvent("start publish") + err = pi.asyncPublisher.Publish(ctx, provider, contextID, digests, meta) + if err != nil { + if errors.Is(err, publisher.ErrAlreadyAdvertised) { + // skipping is ok in this case + pi.log.Warnf("Skipping previously published advert") + return nil + } + + return fmt.Errorf("publishing advert: %w", err) + } + return nil +} + +func filter[T any](results []T, filterFunc func(T) (bool, error)) ([]T, error) { + + filtered := make([]T, 0, len(results)) + for _, result := range results { + include, err := filterFunc(result) + if err != nil { + return nil, err + } + if include { + filtered = append(filtered, result) + } + } + return filtered, nil +} + +// filterableByContextID determines if the metadata can be filtered using a +// [types.ContextID]. +func filterableByContextID(result model.ProviderResult) bool { + md := metadata.MetadataContext.New() + err := md.UnmarshalBinary(result.Metadata) + if err != nil { + return false + } + // we're only able to filter results with location commitment metadata atm + lcommMeta := md.Get(metadata.LocationCommitmentID) + return lcommMeta != nil +} diff --git a/indexing-service/pkg/service/providerindex/providerindex_test.go b/indexing-service/pkg/service/providerindex/providerindex_test.go new file mode 100644 index 00000000..5945c92b --- /dev/null +++ b/indexing-service/pkg/service/providerindex/providerindex_test.go @@ -0,0 +1,167 @@ +package providerindex + +import ( + "context" + "errors" + "iter" + "slices" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/extmocks" + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + "github.com/fil-forge/go-ipni-tools/pkg/publisher" + "github.com/ipni/go-libipni/find/model" + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestGetProviderResults(t *testing.T) { + t.Run("results found in the cache", func(t *testing.T) { + mockStore := types.NewMockProviderStore(t) + mockNoProviderStore := types.NewMockNoProviderStore(t) + mockIpniFinder := extmocks.NewMockIpniFinder(t) + mockIpniPublisher := extmocks.NewMockIpniPublisher(t) + + providerIndex := New(mockStore, mockNoProviderStore, mockIpniFinder, mockIpniPublisher) + + someHash := testutil.RandomMultihash(t) + expectedResult := testutil.RandomLocationCommitmentProviderResult(t) + + mockStore.EXPECT().Members(extmocks.AnyContext, someHash).Return([]model.ProviderResult{expectedResult}, nil) + + results, err := providerIndex.getProviderResults(context.Background(), someHash, []multicodec.Code{metadata.LocationCommitmentID}) + + require.NoError(t, err) + require.Equal(t, []model.ProviderResult{expectedResult}, results) + }) + + t.Run("cache hit but doesn't match target claims, falls through to IPNI", func(t *testing.T) { + mockStore := types.NewMockProviderStore(t) + mockNoProviderStore := types.NewMockNoProviderStore(t) + mockIpniFinder := extmocks.NewMockIpniFinder(t) + mockIpniPublisher := extmocks.NewMockIpniPublisher(t) + + providerIndex := New(mockStore, mockNoProviderStore, mockIpniFinder, mockIpniPublisher) + + existingResult := testutil.RandomIndexClaimProviderResult(t) + someHash := testutil.RandomMultihash(t) + expectedResult := testutil.RandomLocationCommitmentProviderResult(t) + ipniFinderResponse := &model.FindResponse{ + MultihashResults: []model.MultihashResult{ + { + Multihash: someHash, + ProviderResults: []model.ProviderResult{expectedResult}, + }, + }, + } + targetClaim := []multicodec.Code{metadata.LocationCommitmentID} + + mockStore.EXPECT().Members(extmocks.AnyContext, someHash).Return([]model.ProviderResult{existingResult}, nil) + mockNoProviderStore.EXPECT().Members(extmocks.AnyContext, someHash).Return(nil, types.ErrKeyNotFound) + mockIpniFinder.EXPECT().Find(extmocks.AnyContext, someHash).Return(ipniFinderResponse, nil) + mockStore.EXPECT().Add(extmocks.AnyContext, someHash, expectedResult).Return(1, nil) + mockStore.EXPECT().SetExpirable(extmocks.AnyContext, someHash, true).Return(nil) + + results, err := providerIndex.getProviderResults(context.Background(), someHash, targetClaim) + + require.NoError(t, err) + require.Equal(t, []model.ProviderResult{expectedResult}, results) + }) + + t.Run("returns an empty slice when no results are found, no providers record gets cached", func(t *testing.T) { + mockStore := types.NewMockProviderStore(t) + mockNoProviderStore := types.NewMockNoProviderStore(t) + mockIpniFinder := extmocks.NewMockIpniFinder(t) + mockIpniPublisher := extmocks.NewMockIpniPublisher(t) + + providerIndex := New(mockStore, mockNoProviderStore, mockIpniFinder, mockIpniPublisher) + + someHash := testutil.RandomMultihash(t) + + mockStore.EXPECT().Members(extmocks.AnyContext, someHash).Return(nil, types.ErrKeyNotFound) + mockNoProviderStore.EXPECT().Members(extmocks.AnyContext, someHash).Return(nil, types.ErrKeyNotFound) + mockIpniFinder.EXPECT().Find(extmocks.AnyContext, someHash).Return(&model.FindResponse{}, nil) + mockNoProviderStore.EXPECT().Add(extmocks.AnyContext, someHash, multicodec.Code(0)).Return(1, nil) + mockNoProviderStore.EXPECT().SetExpirable(extmocks.AnyContext, someHash, true).Return(nil) + + results, err := providerIndex.getProviderResults(context.Background(), someHash, []multicodec.Code{0}) + + require.NoError(t, err) + require.Empty(t, results) + }) + + t.Run("error in cache returns error", func(t *testing.T) { + mockStore := types.NewMockProviderStore(t) + mockNoProviderStore := types.NewMockNoProviderStore(t) + mockIpniFinder := extmocks.NewMockIpniFinder(t) + mockIpniPublisher := extmocks.NewMockIpniPublisher(t) + + providerIndex := New(mockStore, mockNoProviderStore, mockIpniFinder, mockIpniPublisher) + + someHash := testutil.RandomMultihash(t) + + mockStore.EXPECT().Members(extmocks.AnyContext, someHash).Return(nil, errors.New("some error")) + + _, err := providerIndex.getProviderResults(context.Background(), someHash, []multicodec.Code{0}) + + require.Error(t, err) + }) + + t.Run("error from IPNI is logged and no providers are cached", func(t *testing.T) { + mockStore := types.NewMockProviderStore(t) + mockNoProviderStore := types.NewMockNoProviderStore(t) + mockIpniFinder := extmocks.NewMockIpniFinder(t) + mockIpniPublisher := extmocks.NewMockIpniPublisher(t) + + providerIndex := New(mockStore, mockNoProviderStore, mockIpniFinder, mockIpniPublisher) + + someHash := testutil.RandomMultihash(t) + + mockStore.EXPECT().Members(extmocks.AnyContext, someHash).Return(nil, types.ErrKeyNotFound) + mockNoProviderStore.EXPECT().Members(extmocks.AnyContext, someHash).Return(nil, types.ErrKeyNotFound) + mockIpniFinder.EXPECT().Find(extmocks.AnyContext, someHash).Return(nil, errors.New("some error")) + mockNoProviderStore.EXPECT().Add(extmocks.AnyContext, someHash, multicodec.Code(0)).Return(1, nil) + mockNoProviderStore.EXPECT().SetExpirable(extmocks.AnyContext, someHash, true).Return(nil) + + results, err := providerIndex.getProviderResults(context.Background(), someHash, []multicodec.Code{0}) + + require.NoError(t, err) + require.Empty(t, results) + }) +} + +func TestPublish(t *testing.T) { + t.Run("allow skip publish existing advert", func(t *testing.T) { + mockStore := types.NewMockProviderStore(t) + mockBatcher := types.NewMockValueSetCacheBatcher[multihash.Multihash, model.ProviderResult](t) + mockNoProviderStore := types.NewMockNoProviderStore(t) + mockIpniFinder := extmocks.NewMockIpniFinder(t) + mockIpniPublisher := extmocks.NewMockIpniPublisher(t) + + providerIndex := New(mockStore, mockNoProviderStore, mockIpniFinder, mockIpniPublisher) + + result := testutil.RandomLocationCommitmentProviderResult(t) + provider := *result.Provider + contextID := string(result.ContextID) + digest := testutil.RandomMultihash(t) + anyDigestSeq := mock.MatchedBy(func(digests iter.Seq[multihash.Multihash]) bool { + return true + }) + meta := metadata.MetadataContext.New() + err := meta.UnmarshalBinary(result.Metadata) + require.NoError(t, err) + + mockStore.EXPECT().Batch().Return(mockBatcher) + mockBatcher.EXPECT().Add(extmocks.AnyContext, digest, result).Return(nil) + mockBatcher.EXPECT().SetExpirable(extmocks.AnyContext, digest, false).Return(nil) + mockBatcher.EXPECT().Commit(extmocks.AnyContext).Return(nil) + mockIpniPublisher.EXPECT().Publish(extmocks.AnyContext, provider, contextID, anyDigestSeq, meta).Return(publisher.ErrAlreadyAdvertised) + + err = providerIndex.Publish(context.Background(), provider, contextID, slices.Values([]multihash.Multihash{digest}), meta) + require.NoError(t, err) + }) +} diff --git a/indexing-service/pkg/service/providerindex/remotesyncer/remotesyncer.go b/indexing-service/pkg/service/providerindex/remotesyncer/remotesyncer.go new file mode 100644 index 00000000..138681e8 --- /dev/null +++ b/indexing-service/pkg/service/providerindex/remotesyncer/remotesyncer.go @@ -0,0 +1,87 @@ +package remotesyncer + +import ( + "context" + + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/go-ipni-tools/pkg/store" + logging "github.com/ipfs/go-log/v2" + "github.com/ipld/go-ipld-prime" +) + +type Store interface { + store.EntriesReadable + store.AdvertReadable +} + +type RemoteSyncer struct { + providerStore types.ProviderStore + store Store + log logging.EventLogger +} + +type config struct { + log logging.EventLogger +} + +// Option configures the RemoteSyncer. +type Option func(conf *config) + +// WithLogger configures the service to use the passed logger instead of the +// default logger. +func WithLogger(log logging.EventLogger) Option { + return func(conf *config) { + conf.log = log + } +} + +func New(providerStore types.ProviderStore, store Store, options ...Option) *RemoteSyncer { + conf := config{} + for _, option := range options { + option(&conf) + } + if conf.log == nil { + conf.log = logging.Logger("remotesyncer") + } + return &RemoteSyncer{ + providerStore: providerStore, + store: store, + log: conf.log, + } +} + +func (rs *RemoteSyncer) HandleRemoteSync(ctx context.Context, head, prev ipld.Link) { + rs.log.Infof("handling IPNI remote sync from %s to %s", prev, head) + + cur := head + for { + ad, err := rs.store.Advert(ctx, cur) + if err != nil { + rs.log.Errorf("getting advert: %s: %s", cur, err) + return + } + batch := rs.providerStore.Batch() + for d, err := range rs.store.Entries(ctx, ad.Entries) { + if err != nil { + rs.log.Errorf("iterating advert entries: %s (advert) -> %s (entries): %s", cur, ad.Entries, err) + return + } + err := batch.SetExpirable(ctx, d, true) + if err != nil { + rs.log.Errorf("adding digest to batch: %s: %s", d.B58String(), err) + return + } + } + err = batch.Commit(ctx) + if err != nil { + rs.log.Errorf("comitting batch: %s: %s", cur.String(), err) + return + } + if ad.PreviousID == nil || (prev != nil && ad.PreviousID.String() == prev.String()) { + break + } + cur = ad.PreviousID + } + + rs.log.Infof("handled IPNI remote sync from %s to %s", prev, head) +} diff --git a/indexing-service/pkg/service/providerindex/remotesyncer/remotesyncer_test.go b/indexing-service/pkg/service/providerindex/remotesyncer/remotesyncer_test.go new file mode 100644 index 00000000..ce67a718 --- /dev/null +++ b/indexing-service/pkg/service/providerindex/remotesyncer/remotesyncer_test.go @@ -0,0 +1,239 @@ +package remotesyncer + +import ( + "bytes" + "context" + "fmt" + "iter" + "math/rand/v2" + "slices" + "testing" + "time" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/bytemap" + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipni/go-libipni/find/model" + "github.com/ipni/go-libipni/ingest/schema" + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +func dagCBORLink(t *testing.T, n ipld.Node) ipld.Link { + t.Helper() + buf := bytes.NewBuffer(nil) + err := dagcbor.Encode(n, buf) + require.NoError(t, err) + codec := uint64(multicodec.DagCbor) + digest := testutil.Must(multihash.Sum(buf.Bytes(), multihash.SHA2_256, -1))(t) + return cidlink.Link{Cid: cid.NewCidV1(codec, digest)} +} + +func TestHandleRemoteSync(t *testing.T) { + ads := map[string]schema.Advertisement{} + ents := map[string]schema.EntryChunk{} + ipniStore := mockIpniStore{ads, ents} + providerStore := mockProviderStore{ + data: bytemap.NewByteMap[multihash.Multihash, *valueExpirable[[]model.ProviderResult]](-1), + } + + var hd ipld.Link + for range 5 { + ad := schema.Advertisement{} + if hd != nil { + ad.PreviousID = hd + } + + var ec schema.EntryChunk + for range rand.IntN(100) { + digest := testutil.RandomMultihash(t) + ec.Entries = append(ec.Entries, digest) + _, err := providerStore.Add(context.Background(), digest, model.ProviderResult{}) + require.NoError(t, err) + } + n := testutil.Must(ec.ToNode())(t) + eclnk := dagCBORLink(t, n) + ents[eclnk.String()] = ec + ad.Entries = eclnk + + n = testutil.Must(ad.ToNode())(t) + adlink := dagCBORLink(t, n) + ads[adlink.String()] = ad + hd = adlink + } + + prev := ads[hd.String()].PreviousID + + syncer := New(&providerStore, &ipniStore) + syncer.HandleRemoteSync(context.Background(), prev, nil) + requireExpirable(t, &providerStore, &ipniStore, hd, prev, false) + requireExpirable(t, &providerStore, &ipniStore, prev, nil, true) + + syncer.HandleRemoteSync(context.Background(), hd, prev) + requireExpirable(t, &providerStore, &ipniStore, hd, nil, true) +} + +func requireExpirable(t *testing.T, providerStore *mockProviderStore, ipniStore *mockIpniStore, head ipld.Link, tail ipld.Link, expirable bool) { + cur := head + for cur != tail { + ad, err := ipniStore.Advert(context.Background(), cur) + require.NoError(t, err) + + for digest, err := range ipniStore.Entries(context.Background(), ad.Entries) { + require.NoError(t, err) + exp, err := providerStore.GetExpiration(context.Background(), digest) + require.NoError(t, err) + require.Equal(t, expirable, !exp.IsZero()) + } + cur = ad.PreviousID + } +} + +type mockIpniStore struct { + ads map[string]schema.Advertisement + ents map[string]schema.EntryChunk +} + +func (i *mockIpniStore) Advert(ctx context.Context, id datamodel.Link) (schema.Advertisement, error) { + ad, ok := i.ads[id.String()] + if !ok { + return schema.Advertisement{}, fmt.Errorf("not found: %s", id.String()) + } + return ad, nil +} + +func (i *mockIpniStore) Entries(ctx context.Context, root datamodel.Link) iter.Seq2[multihash.Multihash, error] { + return func(yield func(multihash.Multihash, error) bool) { + ent := i.ents[root.String()] + for _, digest := range ent.Entries { + if !yield(digest, nil) { + return + } + } + } +} + +var _ Store = (*mockIpniStore)(nil) + +type valueExpirable[T any] struct { + val T + exp time.Time +} + +type mockProviderStore struct { + data bytemap.ByteMap[multihash.Multihash, *valueExpirable[[]model.ProviderResult]] +} + +func (m *mockProviderStore) Members(ctx context.Context, key multihash.Multihash) ([]model.ProviderResult, error) { + val := m.data.Get(key) + if val == nil { + return nil, types.ErrKeyNotFound + } + if !val.exp.IsZero() && time.Now().After(val.exp) { + return nil, types.ErrKeyNotFound + } + return val.val, nil +} + +func (m *mockProviderStore) Add(ctx context.Context, digest multihash.Multihash, newProviders ...model.ProviderResult) (uint64, error) { + var providers []model.ProviderResult + var expires time.Time + existing := m.data.Get(digest) + if existing != nil { + providers = append(providers, existing.val...) + expires = existing.exp + } + written := uint64(0) + for _, provider := range newProviders { + if !slices.ContainsFunc(providers, func(p model.ProviderResult) bool { return p.Equal(provider) }) { + providers = append(providers, provider) + written++ + } + } + v := valueExpirable[[]model.ProviderResult]{providers, expires} + m.data.Set(digest, &v) + return written, nil +} + +func (m *mockProviderStore) GetExpiration(ctx context.Context, key multihash.Multihash) (time.Time, error) { + val := m.data.Get(key) + if val == nil { + return time.Time{}, types.ErrKeyNotFound + } + return val.exp, nil +} + +func (m *mockProviderStore) SetExpirable(ctx context.Context, key multihash.Multihash, expires bool) error { + val := m.data.Get(key) + if val == nil { + return types.ErrKeyNotFound + } + if expires { + val.exp = time.Now().Add(time.Second * 30) + } else { + val.exp = time.Time{} + } + return nil +} + +func (m *mockProviderStore) Batch() types.ValueSetCacheBatcher[multihash.Multihash, model.ProviderResult] { + return &mockBatcher{store: m} +} + +type mockCommand struct { + op string + key multihash.Multihash + values []model.ProviderResult + expire bool +} + +type mockBatcher struct { + store *mockProviderStore + commands []mockCommand +} + +func (mb *mockBatcher) Add(ctx context.Context, key multihash.Multihash, newProviders ...model.ProviderResult) error { + mb.commands = append(mb.commands, mockCommand{ + op: "add", + key: key, + values: newProviders, + }) + return nil +} + +func (mb *mockBatcher) SetExpirable(ctx context.Context, key multihash.Multihash, expires bool) error { + mb.commands = append(mb.commands, mockCommand{ + op: "setExpirable", + key: key, + expire: expires, + }) + return nil +} + +func (mb *mockBatcher) Commit(ctx context.Context) error { + for _, c := range mb.commands { + switch c.op { + case "add": + _, err := mb.store.Add(ctx, c.key, c.values...) + if err != nil { + return err + } + case "setExpirable": + err := mb.store.SetExpirable(ctx, c.key, c.expire) + if err != nil { + return err + } + } + } + return nil +} + +var _ types.ValueSetCacheBatcher[multihash.Multihash, model.ProviderResult] = (*mockBatcher)(nil) + +var _ types.ProviderStore = (*mockProviderStore)(nil) diff --git a/indexing-service/pkg/service/queryresult/cbor_gen.go b/indexing-service/pkg/service/queryresult/cbor_gen.go new file mode 100644 index 00000000..90b60bc5 --- /dev/null +++ b/indexing-service/pkg/service/queryresult/cbor_gen.go @@ -0,0 +1,359 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package queryresult + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *QueryResult) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 1 + + if t.Result0_1 == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Result0_1 (queryresult.QueryResult0_1) (struct) + if t.Result0_1 != nil { + + if len("index/query/result@0.1") > 8192 { + return xerrors.Errorf("Value in field \"index/query/result@0.1\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("index/query/result@0.1"))); err != nil { + return err + } + if _, err := cw.WriteString(string("index/query/result@0.1")); err != nil { + return err + } + + if err := t.Result0_1.MarshalCBOR(cw); err != nil { + return err + } + } + return nil +} + +func (t *QueryResult) UnmarshalCBOR(r io.Reader) (err error) { + *t = QueryResult{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("QueryResult: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 22) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Result0_1 (queryresult.QueryResult0_1) (struct) + case "index/query/result@0.1": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Result0_1 = new(QueryResult0_1) + if err := t.Result0_1.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Result0_1 pointer: %w", err) + } + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *QueryResult0_1) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Claims ([]cid.Cid) (slice) + if len("claims") > 8192 { + return xerrors.Errorf("Value in field \"claims\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("claims"))); err != nil { + return err + } + if _, err := cw.WriteString(string("claims")); err != nil { + return err + } + + if len(t.Claims) > 8192 { + return xerrors.Errorf("Slice value in field t.Claims was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Claims))); err != nil { + return err + } + for _, v := range t.Claims { + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + + // t.Indexes (map[string]cid.Cid) (map) + if len("indexes") > 8192 { + return xerrors.Errorf("Value in field \"indexes\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("indexes"))); err != nil { + return err + } + if _, err := cw.WriteString(string("indexes")); err != nil { + return err + } + + { + if len(t.Indexes) > 4096 { + return xerrors.Errorf("cannot marshal t.Indexes map too large") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajMap, uint64(len(t.Indexes))); err != nil { + return err + } + + keys := make([]string, 0, len(t.Indexes)) + for k := range t.Indexes { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := t.Indexes[k] + + if len(k) > 8192 { + return xerrors.Errorf("Value in field k was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(k))); err != nil { + return err + } + if _, err := cw.WriteString(string(k)); err != nil { + return err + } + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + } + return nil +} + +func (t *QueryResult0_1) UnmarshalCBOR(r io.Reader) (err error) { + *t = QueryResult0_1{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("QueryResult0_1: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Claims ([]cid.Cid) (slice) + case "claims": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Claims: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Claims = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Claims[i]: %w", err) + } + + t.Claims[i] = c + + } + + } + } + // t.Indexes (map[string]cid.Cid) (map) + case "indexes": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajMap { + return fmt.Errorf("expected a map (major type 5)") + } + if extra > 4096 { + return fmt.Errorf("t.Indexes: map too large") + } + + t.Indexes = make(map[string]cid.Cid, extra) + + for i, l := 0, int(extra); i < l; i++ { + + var k string + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + k = string(sval) + } + + var v cid.Cid + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field v: %w", err) + } + + v = c + + } + + t.Indexes[k] = v + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/indexing-service/pkg/service/queryresult/gen/main.go b/indexing-service/pkg/service/queryresult/gen/main.go new file mode 100644 index 00000000..0e2e131a --- /dev/null +++ b/indexing-service/pkg/service/queryresult/gen/main.go @@ -0,0 +1,42 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/indexing-service/pkg/service/queryresult" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + queryresult.QueryResult{}, + queryresult.QueryResult0_1{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "queryresult", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "queryresult", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/indexing-service/pkg/service/queryresult/json_gen.go b/indexing-service/pkg/service/queryresult/json_gen.go new file mode 100644 index 00000000..789d7fec --- /dev/null +++ b/indexing-service/pkg/service/queryresult/json_gen.go @@ -0,0 +1,390 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package queryresult + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *QueryResult) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Result0_1 (queryresult.QueryResult0_1) (struct) + if t.Result0_1 != nil { + if len("index/query/result@0.1") > 8192 { + return fmt.Errorf("string in field \"index/query/result@0.1\" was too long") + } + if err := jw.WriteString(string("index/query/result@0.1")); err != nil { + return fmt.Errorf("writing string for field \"index/query/result@0.1\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Result0_1.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Result0_1: %w", err) + } + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *QueryResult) UnmarshalDagJSON(r io.Reader) (err error) { + *t = QueryResult{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for QueryResult: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for QueryResult: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for QueryResult: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field QueryResult: string too large") + } + return fmt.Errorf("reading string for field QueryResult: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field QueryResult: %w", err) + } + switch name { + + // t.Result0_1 (queryresult.QueryResult0_1) (struct) + case "index/query/result@0.1": + + { + null, err := jr.PeekNull() + if err != nil { + return fmt.Errorf("peeking null for field t.Result0_1: %w", err) + } + if null { + if err := jr.ReadNull(); err != nil { + return fmt.Errorf("reading null for field t.Result0_1: %w", err) + } + } else { + t.Result0_1 = new(QueryResult0_1) + if err := t.Result0_1.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Result0_1 pointer: %w", err) + } + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for QueryResult: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field QueryResult: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for QueryResult") + } + } + } + + return nil +} +func (t *QueryResult0_1) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := 0 + + // t.Claims ([]cid.Cid) (slice) + if len("claims") > 8192 { + return fmt.Errorf("string in field \"claims\" was too long") + } + if err := jw.WriteString(string("claims")); err != nil { + return fmt.Errorf("writing string for field \"claims\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Claims) > 8192 { + return fmt.Errorf("slice value in field t.Claims was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Claims: %w", err) + } + for i, v := range t.Claims { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Claims: %w", err) + } + } + + if err := jw.WriteCid(v); err != nil { + return fmt.Errorf("writing CID for field v: %w", err) + } + + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Claims: %w", err) + } + + written++ + if written > 0 { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Indexes (map[string]cid.Cid) (map) + if len("indexes") > 8192 { + return fmt.Errorf("string in field \"indexes\" was too long") + } + if err := jw.WriteString(string("indexes")); err != nil { + return fmt.Errorf("writing string for field \"indexes\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + { + if len(t.Indexes) > 4096 { + return fmt.Errorf("cannot marshal t.Indexes map too large") + } + + if err := jw.WriteObjectOpen(); err != nil { + return fmt.Errorf("writing object open for field t.Indexes: %w", err) + } + + keys := make([]string, 0, len(t.Indexes)) + for k := range t.Indexes { + keys = append(keys, k) + } + sort.Strings(keys) + for i, k := range keys { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Indexes: %w", err) + } + } + v := t.Indexes[k] + if len(k) > 8192 { + return fmt.Errorf("string in field k was too long") + } + if err := jw.WriteString(string(k)); err != nil { + return fmt.Errorf("writing string for field k: %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return fmt.Errorf("writing colon for field t.Indexes: %w", err) + } + + if err := jw.WriteCid(v); err != nil { + return fmt.Errorf("writing CID for field v: %w", err) + } + + } + if err := jw.WriteObjectClose(); err != nil { + return fmt.Errorf("writing object close for field t.Indexes: %w", err) + } + } + + written++ + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *QueryResult0_1) UnmarshalDagJSON(r io.Reader) (err error) { + *t = QueryResult0_1{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for QueryResult0_1: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for QueryResult0_1: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for QueryResult0_1: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field QueryResult0_1: string too large") + } + return fmt.Errorf("reading string for field QueryResult0_1: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field QueryResult0_1: %w", err) + } + switch name { + + // t.Claims ([]cid.Cid) (slice) + case "claims": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Claims: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Claims: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Claims: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]cid.Cid, 1) + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field item[0]: %w", err) + } + item[0] = c + + } + t.Claims = append(t.Claims, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Claims: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Claims: slice too large") + } + } + } + + } + + // t.Indexes (map[string]cid.Cid) (map) + case "indexes": + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for field t.Indexes: %w", err) + } + + t.Indexes = map[string]cid.Cid{} + + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for field t.Indexes: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for field t.Indexes: %w", err) + } + } else { + for i, l := 0, 8192; i < l; i++ { + var k string + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field k: string too long") + } + return fmt.Errorf("reading string for field k: %w", err) + } + k = string(sval) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field t.Indexes: %w", err) + } + var v cid.Cid + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field v: %w", err) + } + v = c + + } + t.Indexes[k] = v + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field t.Indexes: %w", err) + } + if close { + break + } + } + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for QueryResult0_1: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field QueryResult0_1: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for QueryResult0_1") + } + } + } + + return nil +} diff --git a/indexing-service/pkg/service/queryresult/queryresult.go b/indexing-service/pkg/service/queryresult/queryresult.go new file mode 100644 index 00000000..a190082f --- /dev/null +++ b/indexing-service/pkg/service/queryresult/queryresult.go @@ -0,0 +1,211 @@ +//go:build !codegen + +package queryresult + +import ( + "bytes" + "fmt" + "io" + "maps" + "slices" + + "github.com/fil-forge/automobile" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/bytemap" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multicodec" + mh "github.com/multiformats/go-multihash" + multihash "github.com/multiformats/go-multihash/core" +) + +type queryResult struct { + root types.Block + model *QueryResult0_1 + blks []types.Block +} + +var _ types.QueryResult = (*queryResult)(nil) + +func (q *queryResult) Blocks() []types.Block { + return q.blks +} + +func (q *queryResult) Claims() []cid.Cid { + return q.model.Claims +} + +func (q *queryResult) Indexes() []cid.Cid { + return slices.Collect(maps.Values(q.model.Indexes)) +} + +func (q *queryResult) Root() types.Block { + return q.root +} + +func Extract(r io.Reader) (types.QueryResult, error) { + roots, carBlocks, err := automobile.Decode(r) + if err != nil { + return nil, fmt.Errorf("extracting car: %w", err) + } + + if len(roots) != 1 { + return nil, types.ErrWrongRootCount + } + + var rootBlock *types.Block + blocks := make([]types.Block, len(carBlocks)) + for i, blk := range carBlocks { + blocks[i] = types.Block(blk) + if blk.Link == roots[0] { + b := types.Block(blk) + rootBlock = &b + } + } + if rootBlock == nil { + return nil, types.ErrNoRootBlock + } + + var queryResultModel QueryResult + if err := queryResultModel.UnmarshalCBOR(bytes.NewReader(rootBlock.Data)); err != nil { + return nil, fmt.Errorf("decoding query result: %w", err) + } + return &queryResult{root: *rootBlock, model: queryResultModel.Result0_1, blks: blocks}, nil +} + +// Build generates a new encodable QueryResult +func Build(claims map[cid.Cid]ucan.Invocation, indexes bytemap.ByteMap[types.EncodedContextID, blobindex.ShardedDagIndex]) (types.QueryResult, error) { + blocks := make([]types.Block, 0, len(claims)+indexes.Size()+1) + + cls := []cid.Cid{} + for _, claim := range claims { + cls = append(cls, claim.Link()) + blocks = append(blocks, types.Block{ + Link: claim.Link(), + Data: claim.Bytes(), + }) + } + + indexesModel := map[string]cid.Cid{} + if indexes.Size() > 0 { + for contextID, index := range indexes.Iterator() { + var buf bytes.Buffer + if err := blobindex.Archive(index, &buf); err != nil { + return nil, fmt.Errorf("archiving index: %w", err) + } + indexLink, err := cid.Prefix{ + Version: 1, + Codec: uint64(multicodec.Car), + MhType: multihash.SHA2_256, + MhLength: -1, + }.Sum(buf.Bytes()) + if err != nil { + return nil, err + } + indexesModel[string(contextID)] = indexLink + blocks = append(blocks, types.Block{Link: indexLink, Data: buf.Bytes()}) + } + } + + queryResultModel := QueryResult{ + Result0_1: &QueryResult0_1{ + Claims: cls, + Indexes: indexesModel, + }, + } + + var rootBuf bytes.Buffer + if err := queryResultModel.MarshalCBOR(&rootBuf); err != nil { + return nil, fmt.Errorf("encoding query result: %w", err) + } + rootLink, err := cid.V1Builder{ + Codec: cid.DagCBOR, + MhType: multihash.SHA2_256, + MhLength: -1, + }.Sum(rootBuf.Bytes()) + if err != nil { + return nil, fmt.Errorf("building root CID: %w", err) + } + rootBlock := types.Block{Link: rootLink, Data: rootBuf.Bytes()} + blocks = append(blocks, rootBlock) + + return &queryResult{root: rootBlock, model: queryResultModel.Result0_1, blks: blocks}, nil +} + +// BuildCompressed returns a QueryResult that, when there is a matching index entry for the +// targetMh, replaces the full index with a single location claim for the targetMh +func BuildCompressed(targetMh mh.Multihash, principal ucan.Issuer, claims map[cid.Cid]ucan.Invocation, indexes bytemap.ByteMap[types.EncodedContextID, blobindex.ShardedDagIndex]) (types.QueryResult, error) { + + // our goal here is to remove indexes from the query result if there are any + // if there are no indexes, we can just build the regular query result + if indexes.Size() == 0 { + return Build(claims, indexes) + } + + for _, index := range indexes.Iterator() { + for shardHash, shard := range index.Shards().Iterator() { + if shard.Has(targetMh) { + pos := shard.Get(targetMh) + hasLocation := false + var locClaim assert.LocationArguments + var expiration *ucan.UnixTimestamp + for _, claim := range claims { + if claim.Command() != assert.Location.Command { + continue + } + if err := locClaim.UnmarshalCBOR(bytes.NewReader(claim.ArgumentsBytes())); err != nil { + continue + } + if !bytes.Equal(locClaim.Content, shardHash) { + continue + } + hasLocation = true + expiration = claim.Expiration() + break + } + if !hasLocation { + continue + } + + start := uint64(pos.Start) + end := uint64(pos.End) + if locClaim.Range != nil { + start = locClaim.Range.Start + uint64(pos.Start) + if locClaim.Range.End != nil { + end = locClaim.Range.Start + uint64(pos.End) + } + } + newArgs := assert.LocationArguments{ + Content: targetMh, + Location: locClaim.Location, + Range: &assert.Range{Start: start, End: &end}, + Space: locClaim.Space, + } + var opts = []invocation.Option{ + invocation.WithAudience(principal.DID()), + } + if expiration != nil { + opts = append(opts, invocation.WithExpiration(*expiration)) + } + + claim, err := assert.Location.Invoke( + principal, + principal.DID(), + &newArgs, + opts..., + ) + if err != nil { + return nil, fmt.Errorf("delegating compressed location claim: %w", err) + } + + newClaims := map[cid.Cid]ucan.Invocation{claim.Link(): claim} + return Build(newClaims, bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](-1)) + } + } + } + // never found the MH in any index shard, just build the regular query result + return Build(claims, indexes) +} diff --git a/indexing-service/pkg/service/queryresult/queryresult_test.go b/indexing-service/pkg/service/queryresult/queryresult_test.go new file mode 100644 index 00000000..d431b977 --- /dev/null +++ b/indexing-service/pkg/service/queryresult/queryresult_test.go @@ -0,0 +1,239 @@ +package queryresult + +import ( + "bytes" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/bytemap" + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" +) + +func TestBuildCompressed(t *testing.T) { + t.Run("compresses with matching index entry", func(t *testing.T) { + principal := testutil.RandomIssuer(t) + targetMh := testutil.RandomMultihash(t) + index := blobindex.NewShardedDagIndex(1) + shardMh := testutil.RandomMultihash(t) + + targetRange := blobindex.Range{Start: 100, End: 149} + index.SetSlice(shardMh, targetMh, targetRange) + + for i := 0; i < 5; i++ { + index.SetSlice(shardMh, testutil.RandomMultihash(t), blobindex.Range{ + Start: int64(200 + i*100), + End: int64(200 + i*100 + 74), + }) + } + + indexHash := shardMh + + shardLength := uint64(5000) + shardClaim, err := assert.Location.Invoke( + principal, + principal.DID(), + &assert.LocationArguments{ + Space: principal.DID(), + Content: shardMh, + Location: []commands.CborURL{commands.CborURL(*testutil.TestURL)}, + Range: &assert.Range{ + Start: 1000, + End: &shardLength, + }, + }, + ) + require.NoError(t, err) + + otherShardLength := uint64(8000) + otherShardMH := testutil.RandomMultihash(t) + otherShardClaim, err := assert.Location.Invoke( + principal, + principal.DID(), + &assert.LocationArguments{ + Space: principal.DID(), + Content: otherShardMH, + Location: []commands.CborURL{commands.CborURL(*testutil.TestURL)}, + Range: &assert.Range{ + Start: 1000, + End: &otherShardLength, + }, + }, + ) + require.NoError(t, err) + + claims := map[cid.Cid]ucan.Invocation{ + otherShardClaim.Link(): otherShardClaim, + shardClaim.Link(): shardClaim, + } + + indexes := bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](1) + indexContextID, err := types.ContextID{Hash: indexHash}.ToEncoded() + require.NoError(t, err) + indexes.Set(indexContextID, index) + + result, err := BuildCompressed(targetMh, principal, claims, indexes) + require.NoError(t, err) + + resultClaims := result.Claims() + require.Len(t, resultClaims, 1, "should have exactly one claim") + + resultIndexes := result.Indexes() + require.Len(t, resultIndexes, 0, "should have no indexes") + + var compressedBlock *types.Block + for _, blk := range result.Blocks() { + if blk.Link == resultClaims[0] { + b := blk + compressedBlock = &b + break + } + } + require.NotNil(t, compressedBlock, "should find the compressed claim block") + + decoded, err := invocation.Decode(compressedBlock.Data) + require.NoError(t, err) + require.Equal(t, assert.Location.Command, decoded.Command()) + + var args assert.LocationArguments + require.NoError(t, args.UnmarshalCBOR(bytes.NewReader(decoded.ArgumentsBytes()))) + require.True(t, bytes.Equal(args.Content, targetMh)) + require.NotNil(t, args.Range) + require.Equal(t, uint64(1000)+uint64(targetRange.Start), args.Range.Start) + require.NotNil(t, args.Range.End) + require.Equal(t, uint64(1000)+uint64(targetRange.End), *args.Range.End) + }) + + t.Run("doesn't fail when matching location claim doesn't have a range", func(t *testing.T) { + principal := testutil.RandomIssuer(t) + targetMh := testutil.RandomMultihash(t) + index := blobindex.NewShardedDagIndex(1) + shardMh := testutil.RandomMultihash(t) + + targetRange := blobindex.Range{Start: 100, End: 149} + index.SetSlice(shardMh, targetMh, targetRange) + + for i := 0; i < 5; i++ { + index.SetSlice(shardMh, testutil.RandomMultihash(t), blobindex.Range{ + Start: int64(200 + i*100), + End: int64(200 + i*100 + 74), + }) + } + + indexHash := shardMh + + shardClaim, err := assert.Location.Invoke( + principal, + principal.DID(), + &assert.LocationArguments{ + Space: principal.DID(), + Content: shardMh, + Location: []commands.CborURL{commands.CborURL(*testutil.TestURL)}, + Range: nil, + }, + ) + require.NoError(t, err) + + claims := map[cid.Cid]ucan.Invocation{ + shardClaim.Link(): shardClaim, + } + + indexes := bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](1) + indexContextID, err := types.ContextID{Hash: indexHash}.ToEncoded() + require.NoError(t, err) + indexes.Set(indexContextID, index) + + result, err := BuildCompressed(targetMh, principal, claims, indexes) + require.NoError(t, err) + + resultClaims := result.Claims() + require.Len(t, resultClaims, 1, "should have exactly one claim") + + resultIndexes := result.Indexes() + require.Len(t, resultIndexes, 0, "should have no indexes") + + var compressedBlock *types.Block + for _, blk := range result.Blocks() { + if blk.Link == resultClaims[0] { + b := blk + compressedBlock = &b + break + } + } + require.NotNil(t, compressedBlock) + + decoded, err := invocation.Decode(compressedBlock.Data) + require.NoError(t, err) + var args assert.LocationArguments + require.NoError(t, args.UnmarshalCBOR(bytes.NewReader(decoded.ArgumentsBytes()))) + require.True(t, bytes.Equal(args.Content, targetMh)) + require.NotNil(t, args.Range) + require.Equal(t, uint64(targetRange.Start), args.Range.Start) + require.NotNil(t, args.Range.End) + require.Equal(t, uint64(targetRange.End), *args.Range.End) + }) + + t.Run("returns regular result when no matching index entry", func(t *testing.T) { + principal := testutil.RandomIssuer(t) + targetMh := testutil.RandomMultihash(t) + index := blobindex.NewShardedDagIndex(1) + + shardMh := testutil.RandomMultihash(t) + for i := 0; i < 5; i++ { + index.SetSlice(shardMh, testutil.RandomMultihash(t), blobindex.Range{ + Start: int64(100 + i*100), + End: int64(100 + i*100 + 49), + }) + } + + indexHash := shardMh + + locationClaim := testutil.RandomLocationCommitment(t) + indexClaim := testutil.RandomIndexClaim(t) + claims := map[cid.Cid]ucan.Invocation{ + locationClaim.Link(): locationClaim, + indexClaim.Link(): indexClaim, + } + + indexes := bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](1) + indexContextID, err := types.ContextID{Hash: indexHash}.ToEncoded() + require.NoError(t, err) + indexes.Set(indexContextID, index) + + result, err := BuildCompressed(targetMh, principal, claims, indexes) + require.NoError(t, err) + + resultClaims := result.Claims() + require.Len(t, resultClaims, 2, "should have both original claims") + + resultIndexes := result.Indexes() + require.Len(t, resultIndexes, 1, "should have the original index") + }) + + t.Run("returns regular result when no indexes", func(t *testing.T) { + principal := testutil.RandomIssuer(t) + targetMh := testutil.RandomMultihash(t) + + locationClaim := testutil.RandomLocationCommitment(t) + claims := map[cid.Cid]ucan.Invocation{ + locationClaim.Link(): locationClaim, + } + + indexes := bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](-1) + + result, err := BuildCompressed(targetMh, principal, claims, indexes) + require.NoError(t, err) + + resultClaims := result.Claims() + require.Len(t, resultClaims, 1) + + resultIndexes := result.Indexes() + require.Len(t, resultIndexes, 0) + }) +} diff --git a/indexing-service/pkg/service/queryresult/types.go b/indexing-service/pkg/service/queryresult/types.go new file mode 100644 index 00000000..be7b8be0 --- /dev/null +++ b/indexing-service/pkg/service/queryresult/types.go @@ -0,0 +1,14 @@ +package queryresult + +import "github.com/ipfs/go-cid" + +// QueryResult is the golang structure for encoding query results +type QueryResult struct { + Result0_1 *QueryResult0_1 `cborgen:"index/query/result@0.1,omitempty" dagjsongen:"index/query/result@0.1,omitempty"` +} + +// QueryResult0_1 describes the found claims and indexes for a given query +type QueryResult0_1 struct { + Claims []cid.Cid `cborgen:"claims" dagjsongen:"claims"` + Indexes map[string]cid.Cid `cborgen:"indexes" dagjsongen:"indexes"` +} diff --git a/indexing-service/pkg/service/service.go b/indexing-service/pkg/service/service.go new file mode 100644 index 00000000..5b1cc885 --- /dev/null +++ b/indexing-service/pkg/service/service.go @@ -0,0 +1,846 @@ +package service + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/url" + "slices" + "strings" + "sync" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/jobwalker" + "github.com/fil-forge/forge/indexing-service/pkg/internal/jobwalker/parallelwalk" + "github.com/fil-forge/forge/indexing-service/pkg/internal/jobwalker/singlewalk" + "github.com/fil-forge/forge/indexing-service/pkg/service/blobindexlookup" + "github.com/fil-forge/forge/indexing-service/pkg/service/contentclaims" + "github.com/fil-forge/forge/indexing-service/pkg/service/providerindex" + "github.com/fil-forge/forge/indexing-service/pkg/service/queryresult" + "github.com/fil-forge/forge/indexing-service/pkg/telemetry" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/internal/ipni/advertisement" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/bytemap" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/content" + "github.com/fil-forge/forge/protocol/digestutil" + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + "github.com/fil-forge/ucantone/did" + ed25519_verifier "github.com/fil-forge/ucantone/multikey/ed25519/verifier" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" + ucanlib "github.com/fil-forge/ucantone/ucanlib" + "github.com/fil-forge/ucantone/validator" + "github.com/ipfs/go-cid" + logging "github.com/ipfs/go-log/v2" + "github.com/ipni/go-libipni/find/model" + "github.com/ipni/go-libipni/maurl" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" + "go.opentelemetry.io/otel/attribute" +) + +const ( + ClaimUrlPlaceholder = "{claim}" + blobUrlPlaceholder = "{blob}" + blobCIDUrlPlaceholder = "{blobCID}" +) + +var log = logging.Logger("service") + +var ErrUnrecognizedClaim = errors.New("unrecognized claim type") + +// IndexingService implements read/write logic for indexing data with IPNI, content claims, sharded dag indexes, and a cache layer +type IndexingService struct { + id ucan.Issuer + blobIndexLookup blobindexlookup.BlobIndexLookup + claims contentclaims.Service + providerIndex providerindex.ProviderIndex + // provider is the peer info for this service, used when publishing claims. + provider peer.AddrInfo + jobWalker jobwalker.JobWalker[job, queryState] +} + +var _ types.Service = (*IndexingService)(nil) + +type job struct { + mh multihash.Multihash + indexForMh *multihash.Multihash + indexProviderRecord *model.ProviderResult + queryType types.QueryType +} + +type jobKey string + +func (j job) key() jobKey { + k := jobKey(j.mh) + jobKey(j.queryType.String()) + if j.indexForMh != nil { + k += jobKey(*j.indexForMh) + } + return k +} + +var targetClaims = map[types.QueryType][]multicodec.Code{ + types.QueryTypeStandard: {metadata.EqualsClaimID, metadata.IndexClaimID, metadata.LocationCommitmentID}, + types.QueryTypeStandardCompressed: {metadata.EqualsClaimID, metadata.IndexClaimID, metadata.LocationCommitmentID}, + types.QueryTypeLocation: {metadata.LocationCommitmentID}, + types.QueryTypeIndexOrLocation: {metadata.IndexClaimID, metadata.LocationCommitmentID}, +} + +type queryResult struct { + Claims map[cid.Cid]ucan.Invocation + Indexes bytemap.ByteMap[types.EncodedContextID, blobindex.ShardedDagIndex] +} + +type queryState struct { + q *types.Query + qr *queryResult + visits map[jobKey]struct{} +} + +func (is *IndexingService) jobHandler(mhCtx context.Context, j job, spawn func(job) error, state jobwalker.WrappedState[queryState]) error { + mhCtx, s := telemetry.StartSpan(mhCtx, "IndexingService.jobHandler") + defer s.End() + s.SetAttributes(attribute.String("multihash", digestutil.Format(j.mh))) + + // check if node has already been visited and ignore if that is the case + if !state.CmpSwap(func(qs queryState) bool { + _, ok := qs.visits[j.key()] + return !ok + }, func(qs queryState) queryState { + qs.visits[j.key()] = struct{}{} + return qs + }) { + return nil + } + + // find provider records related to this multihash + s.AddEvent("finding relevant ProviderResults") + results, err := is.providerIndex.Find(mhCtx, providerindex.QueryKey{ + Hash: j.mh, + Spaces: state.Access().q.Match.Subject, + TargetClaims: targetClaims[j.queryType], + }) + if err != nil { + telemetry.Error(s, err, "finding ProviderResults") + return fmt.Errorf("finding provider results: %w", err) + } + + s.AddEvent(fmt.Sprintf("processing %d results", len(results))) + + var indexFetchSucceeded bool + var lastIndexFetchErr error + + for _, result := range results { + // unmarshall metadata for this provider + md := metadata.MetadataContext.New() + err = md.UnmarshalBinary(result.Metadata) + if err != nil { + telemetry.Error(s, err, "unmarshaling metadata") + return fmt.Errorf("unmarshaling metadata: %w", err) + } + + // the provider may list one or more protocols for this CID + // in our case, the protocols are just differnt types of content claims + for _, code := range md.Protocols() { + protocol := md.Get(code) + // make sure this is some kind of claim protocol, ignore if not + hasClaimCid, ok := protocol.(metadata.HasClaim) + if !ok { + continue + } + + // fetch (from cache or url) the actual content claim + claimCid := hasClaimCid.GetClaim() + log.Infow("query: fetching claim", "claimCid", claimCid, "providerId", result.Provider.ID, "numAddrs", len(result.Provider.Addrs)) + for i, addr := range result.Provider.Addrs { + log.Infow("query: provider address", "index", i, "addr", addr.String()) + } + url, err := fetchClaimURL(*result.Provider, claimCid) + if err != nil { + log.Errorw("query: failed to build claim URL", "error", err) + telemetry.Error(s, err, "building claim URL") + return fmt.Errorf("fetching claim URL: %w", err) + } + + s.AddEvent("fetching claims") + claim, err := is.claims.Find(mhCtx, claimCid, url) + if err != nil { + telemetry.Error(s, err, "fetching claims") + return fmt.Errorf("fetching claims: %w", err) + } + // add the fetched claim to the results, if we don't already have it + state.CmpSwap( + func(qs queryState) bool { + _, ok := qs.qr.Claims[claimCid] + return !ok + }, + func(qs queryState) queryState { + qs.qr.Claims[claimCid] = claim + return qs + }) + + // handle each type of protocol + switch typedProtocol := protocol.(type) { + case *metadata.EqualsClaimMetadata: + s.AddEvent("processing equals claim") + + // for an equals claim, it's published on both the content and equals multihashes + // we follow with a query for location claim on the OTHER side of the multihash + if string(typedProtocol.Equals.Hash()) != string(j.mh) { + // lookup was the content hash, queue the equals hash + if err := spawn(job{typedProtocol.Equals.Hash(), nil, nil, types.QueryTypeLocation}); err != nil { + telemetry.Error(s, err, "queing job for equals hash") + return fmt.Errorf("queuing job for equals hash: %w", err) + } + } else { + // lookup was the equals hash, queue the content hash + if err := spawn(job{multihash.Multihash(result.ContextID), nil, nil, types.QueryTypeLocation}); err != nil { + telemetry.Error(s, err, "queuing job for content hash") + return fmt.Errorf("queuing job for content hash: %w", err) + } + } + case *metadata.IndexClaimMetadata: + s.AddEvent("processing index claim") + + // for an index claim, we follow by looking for a location claim for the index, and fetching the index + mh := j.mh + if err := spawn(job{typedProtocol.Index.Hash(), &mh, &result, types.QueryTypeLocation}); err != nil { + telemetry.Error(s, err, "queuing job for the index's location claim") + return fmt.Errorf("queuing job for index location claim: %w", err) + } + case *metadata.LocationCommitmentMetadata: + s.AddEvent("processing location claim") + + // for a location claim, we just store it, unless its for an index CID, in which case get the full index + if j.indexForMh != nil { + // Try to fetch the index from this provider result + // If it fails, we'll continue to the next result instead of failing the entire query + shard := typedProtocol.Shard + if shard == nil { + c := cid.NewCidV1(cid.Raw, j.mh) + shard = &c + } + url, err := fetchRetrievalURL(*result.Provider, *shard) + if err != nil { + telemetry.Error(s, err, "fetching index retrieval URL") + log.Warnw("failed to fetch retrieval URL, will try next provider result if available", "shard", shard, "provider", result.Provider.ID, "err", err) + lastIndexFetchErr = fmt.Errorf("fetching retrieval URL for index %q from provider %s: %w", shard, result.Provider.ID, err) + continue // Try next provider result + } + + s.AddEvent("fetching index") + var lcArgs assert.LocationArguments + if err := lcArgs.UnmarshalCBOR(bytes.NewReader(claim.ArgumentsBytes())); err != nil { + log.Warnw("failed to match claim to location commitment, will try next provider result if available", "err", err) + lastIndexFetchErr = fmt.Errorf("failed to match claim to location commitment: %w", err) + continue + } + + space := lcArgs.Space + dlgs := state.Access().q.Delegations + // Authorized retrieval requires a space in the location claim, a + // delegation for the retrieval, and an absolute byte range to extract. + if lcArgs.Range == nil || lcArgs.Range.End == nil { + log.Warnw("missing range in location commitment, will try next provider result if available", "err", err) + lastIndexFetchErr = fmt.Errorf("missing range in location commitment for blob: %s", shard) + continue + } + + proofStore := ucanlib.NewContainerProofStore(container.New(container.WithDelegations(dlgs...))) + proofs, _, err := proofStore.ProofChain(mhCtx, is.id.DID(), content.Retrieve.Command, space) + if err != nil { + log.Warnw("failed to build proof chain, will try next provider result if available", "err", err) + lastIndexFetchErr = fmt.Errorf("failed to build proof chain: %w", err) + continue + } + // ProofChain returns (nil, nil) when no chain matches; the + // proofs[0].Subject() access below would otherwise panic. + // Skip this provider — a later result might supply a usable + // chain, or the loop will fall through with a useful error. + if len(proofs) == 0 { + log.Warnw("no /content/retrieve proof chain in query delegations, will try next provider result if available", + "audience", is.id.DID().String(), + "subject", space.String(), + ) + lastIndexFetchErr = fmt.Errorf("no /content/retrieve proof chain for audience %s subject %s", is.id.DID(), space) + continue + } + + contentRange := content.Range{ + Start: lcArgs.Range.Start, + End: *lcArgs.Range.End, + } + req := types.RetrievalRequest{ + URL: *url, + Range: &contentRange, + Auth: types.RetrievalAuth{ + Issuer: is.id, + Audience: claim.Issuer(), + Command: content.Retrieve.Command, + Subject: proofs[0].Subject(), + Arguments: &content.RetrieveArguments{ + Blob: content.Blob{Digest: lcArgs.Content}, + Range: contentRange, + }, + Proofs: proofs, + }, + } + + index, err := is.blobIndexLookup.Find(mhCtx, result.ContextID, *j.indexProviderRecord, req) + if err != nil { + telemetry.Error(s, err, "fetching index blob") + log.Warnw("failed to fetch index blob, will try next provider result if available", "provider", result.Provider.ID, "err", err) + lastIndexFetchErr = fmt.Errorf("fetching index blob from provider %s: %w", result.Provider.ID, err) + continue // Try next provider result + } + + // Success! Add the index to the query results, if we don't already have it + indexFetchSucceeded = true + state.CmpSwap( + func(qs queryState) bool { + return !qs.qr.Indexes.Has(result.ContextID) + }, + func(qs queryState) queryState { + qs.qr.Indexes.Set(result.ContextID, index) + return qs + }) + + // add location queries for all shards containing the original CID we're seeing an index for + s.AddEvent("adding location queries for indexed shards") + shards := index.Shards().Iterator() + for shard, index := range shards { + if index.Has(*j.indexForMh) { + if err := spawn(job{shard, nil, nil, types.QueryTypeLocation}); err != nil { + telemetry.Error(s, err, "queuing location job for shard") + return fmt.Errorf("queuing location job for shard: %w", err) + } + } + } + } + } + } + } + + // If we attempted to fetch an index but all attempts failed, return the last error + if lastIndexFetchErr != nil && !indexFetchSucceeded { + return fmt.Errorf("failed to fetch index from all provider results: %w", lastIndexFetchErr) + } + return nil +} + +// Query returns back relevant content claims for the given query using the following steps +// 1. Query the ProviderIndex for all matching records +// 2. For any index claims, query the ProviderIndex for location claims for the index cid +// 3. Query the BlobIndexLookup to get the full ShardedDagIndex for any index claims +// 4. Query ProviderIndex for any location claims for any shards that contain the multihash based on the ShardedDagIndex +// 5. Read the requisite claims from the ClaimLookup +// 6. Return all discovered claims and sharded dag indexes +func (is *IndexingService) Query(ctx context.Context, q types.Query) (types.QueryResult, error) { + ctx, s := telemetry.StartSpan(ctx, "IndexingService.Query") + defer s.End() + + if q.Type == types.QueryTypeStandardCompressed && len(q.Hashes) != 1 { + return nil, fmt.Errorf("invalid query: expected 1 hash for compressed query, got %d", len(q.Hashes)) + } + + initialJobs := make([]job, 0, len(q.Hashes)) + for _, mh := range q.Hashes { + initialJobs = append(initialJobs, job{mh, nil, nil, q.Type}) + } + qs, err := is.jobWalker(ctx, initialJobs, queryState{ + q: &q, + qr: &queryResult{ + Claims: make(map[cid.Cid]ucan.Invocation), + Indexes: bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](-1), + }, + visits: map[jobKey]struct{}{}, + }, is.jobHandler) + if err != nil { + return nil, err + } + if q.Type == types.QueryTypeStandardCompressed { + return queryresult.BuildCompressed(q.Hashes[0], is.id, qs.qr.Claims, qs.qr.Indexes) + } + return queryresult.Build(qs.qr.Claims, qs.qr.Indexes) +} + +type replacement struct { + resourcePlaceholder string + resourceID string +} + +func urlForResource(provider peer.AddrInfo, replacements []replacement) (*url.URL, error) { + for _, addr := range provider.Addrs { + // first, attempt to convert the addr to a url scheme + url, err := maurl.ToURL(addr) + // if it can't be converted, skip + if err != nil { + continue + } + // must be an http url + if !(url.Scheme == "http" || url.Scheme == "https") { + continue + } + replacedAny := false + for _, replacement := range replacements { + resourcePlaceholder, resourceID := replacement.resourcePlaceholder, replacement.resourceID + // we must have a place to place the resourceId in the path + if !strings.Contains(url.Path, resourcePlaceholder) { + continue + } + replacedAny = true + // ok we have a matching URL, return with all resource placeholders replaced with the id + url.Path = strings.ReplaceAll(url.Path, resourcePlaceholder, resourceID) + } + if replacedAny { + // maurl.ToURL produces a path without a leading slash because + // multiaddrs are path-segment based. URL.String() papers this + // over when rendering, but URL.RequestURI() (which + // http.Request.Write uses on the wire) reproduces the path + // verbatim — and an HTTP/1.1 request-target without a leading + // slash is malformed, causing Go's net/http server to reject + // it with a bare "400 Bad Request" before any handler runs. + // Normalize here. + if !strings.HasPrefix(url.Path, "/") { + url.Path = "/" + url.Path + } + return url, nil + } + } + + placeholders := make([]string, 0, len(replacements)) + for _, r := range replacements { + placeholders = append(placeholders, r.resourcePlaceholder) + } + + addrs := make([]string, 0, len(provider.Addrs)) + for _, addr := range provider.Addrs { + addrs = append(addrs, addr.String()) + } + + return nil, fmt.Errorf("no %s endpoint found in %d addresses: %s", strings.Join(placeholders, " or "), len(addrs), strings.Join(addrs, ", ")) +} + +func fetchClaimURL(provider peer.AddrInfo, claimCid cid.Cid) (*url.URL, error) { + return urlForResource(provider, []replacement{{ClaimUrlPlaceholder, claimCid.String()}}) +} + +func fetchRetrievalURL(provider peer.AddrInfo, shard cid.Cid) (*url.URL, error) { + return urlForResource(provider, []replacement{{blobUrlPlaceholder, digestutil.Format(shard.Hash())}, {blobCIDUrlPlaceholder, shard.String()}}) +} + +func (is *IndexingService) Get(ctx context.Context, claim cid.Cid) (ucan.Invocation, error) { + return is.claims.Get(ctx, claim) +} + +// Cache is used to cache a claim without publishing it to IPNI +// this is used cache a location commitment that come from a storage provider on blob/accept, without publishing, since the SP will publish themselves +// (a delegation for a location commitment is already generated on blob/accept) +// ideally however, IPNI would enable UCAN chains for publishing so that we could publish it directly from the storage service +// it doesn't for now, so we let SPs publish themselves them direct cache with us +func (is *IndexingService) Cache(ctx context.Context, provider peer.AddrInfo, claim ucan.Invocation, meta ucan.Container) error { + return Cache(ctx, is.blobIndexLookup, is.claims, is.providerIndex, provider, claim, meta) +} + +// Publish caches and publishes a content claim +// I imagine publish claim to work as follows +// For all claims except index, just use the publish API on ProviderIndex +// For index claims, let's assume they fail if a location claim for the index car cid is not already published +// The service should lookup the index cid location claim, and fetch the ShardedDagIndexView, then use the hashes inside +// to assemble all the multihashes in the index advertisement +func (is *IndexingService) Publish(ctx context.Context, claim ucan.Invocation, meta ucan.Container) error { + return Publish(ctx, is.id, is.blobIndexLookup, is.claims, is.providerIndex, is.provider, claim, meta) +} + +// Option configures an IndexingService +type Option func(is *IndexingService) + +// WithConcurrency causes the indexing service to process find queries parallel, with the given concurrency +func WithConcurrency(concurrency int) Option { + return func(is *IndexingService) { + is.jobWalker = parallelwalk.NewParallelWalk[job, queryState](concurrency) + } +} + +// NewIndexingService returns a new indexing service +func NewIndexingService(id ucan.Issuer, blobIndexLookup blobindexlookup.BlobIndexLookup, claims contentclaims.Service, publicAddrInfo peer.AddrInfo, providerIndex providerindex.ProviderIndex, options ...Option) *IndexingService { + provider := peer.AddrInfo{ID: publicAddrInfo.ID} + for _, addr := range publicAddrInfo.Addrs { + claimSuffix, _ := multiaddr.NewMultiaddr("/http-path/" + url.PathEscape("claim/"+ClaimUrlPlaceholder)) + provider.Addrs = append(provider.Addrs, multiaddr.Join(addr, claimSuffix)) + } + is := &IndexingService{ + id: id, + blobIndexLookup: blobIndexLookup, + claims: claims, + provider: provider, + providerIndex: providerIndex, + jobWalker: singlewalk.SingleWalker[job, queryState], + } + for _, option := range options { + option(is) + } + return is +} + +func Cache(ctx context.Context, blobIndex blobindexlookup.BlobIndexLookup, claims contentclaims.Service, provIndex providerindex.ProviderIndex, provider peer.AddrInfo, claim ucan.Invocation, meta ucan.Container) error { + ctx, s := telemetry.StartSpan(ctx, "IndexingService.Cache") + defer s.End() + + switch claim.Command() { + case assert.Location.Command: + s.SetAttributes(attribute.KeyValue{Key: "claim", Value: attribute.StringValue("assert/location")}) + return cacheLocationCommitment(ctx, claims, provIndex, provider, claim, meta) + default: + return ErrUnrecognizedClaim + } +} + +func cacheLocationCommitment(ctx context.Context, claims contentclaims.Service, provIndex providerindex.ProviderIndex, provider peer.AddrInfo, claim ucan.Invocation, _ ucan.Container) error { + if claim.Command() != assert.Location.Command { + return fmt.Errorf("invalid claim type: expected assert/location, got %s", claim.Command().String()) + } + + var args assert.LocationArguments + if err := args.UnmarshalCBOR(bytes.NewReader(claim.ArgumentsBytes())); err != nil { + return fmt.Errorf("unmarshaling location claim arguments: %w", err) + } + + err := claims.Cache(ctx, claim) + if err != nil { + return fmt.Errorf("caching claim with claim service: %w", err) + } + + digests := []multihash.Multihash{args.Content} + contextID, err := advertisement.EncodeContextID(args.Space, args.Content) + if err != nil { + return fmt.Errorf("encoding advertisement context ID: %w", err) + } + + var exp ucan.UnixTimestamp + if claim.Expiration() != nil { + exp = *claim.Expiration() + } + + var rng *metadata.Range + if args.Range != nil { + rng = &metadata.Range{Offset: args.Range.Start} + if args.Range.End != nil { + length := *args.Range.End - args.Range.Start + 1 + rng.Length = &length + } + } + + shardCid, err := advertisement.ShardCID(provider, args) + if err != nil { + return fmt.Errorf("failed to extract shard CID for provider: %s locationCommitment %v: %w", provider, args, err) + } + + meta := metadata.MetadataContext.New( + &metadata.LocationCommitmentMetadata{ + Shard: shardCid, + Expiration: int64(exp), + Claim: claim.Link(), + Range: rng, + }, + ) + + err = provIndex.Cache(ctx, provider, string(contextID), slices.Values(digests), meta) + if err != nil { + return fmt.Errorf("caching claim with provider index: %w", err) + } + + return nil +} + +func Publish(ctx context.Context, id ucan.Issuer, blobIndex blobindexlookup.BlobIndexLookup, claims contentclaims.Service, provIndex providerindex.ProviderIndex, provider peer.AddrInfo, claim ucan.Invocation, meta ucan.Container) error { + ctx, s := telemetry.StartSpan(ctx, "IndexingService.Publish") + defer s.End() + + switch claim.Command() { + case assert.Equals.Command: + s.SetAttributes(attribute.KeyValue{Key: "claim", Value: attribute.StringValue(assert.Equals.String())}) + return publishEqualsClaim(ctx, claims, provIndex, provider, claim, meta) + case assert.Index.Command: + s.SetAttributes(attribute.KeyValue{Key: "claim", Value: attribute.StringValue(assert.Index.String())}) + return publishIndexClaim(ctx, id, blobIndex, claims, provIndex, provider, claim, meta) + default: + return ErrUnrecognizedClaim + } +} + +func publishEqualsClaim(ctx context.Context, claims contentclaims.Service, provIndex providerindex.ProviderIndex, provider peer.AddrInfo, claim ucan.Invocation, _ ucan.Container) error { + var args assert.EqualsArguments + if err := args.UnmarshalCBOR(bytes.NewReader(claim.ArgumentsBytes())); err != nil { + return fmt.Errorf("unmarshaling equals claim arguments: %w", err) + } + + err := claims.Publish(ctx, claim) + if err != nil { + return fmt.Errorf("caching equals claim with claim service: %w", err) + } + + var exp ucan.UnixTimestamp + if claim.Expiration() != nil { + exp = *claim.Expiration() + } + + meta := metadata.MetadataContext.New( + &metadata.EqualsClaimMetadata{ + Equals: args.Equals, + Expiration: int64(exp), + Claim: claim.Link(), + }, + ) + + var digests []multihash.Multihash + digests = append(digests, args.Content) + digests = append(digests, args.Equals.Hash()) + contextID := string(args.Content) + err = provIndex.Publish(ctx, provider, contextID, slices.Values(digests), meta) + if err != nil { + return fmt.Errorf("publishing equals claim: %w", err) + } + + return nil +} + +func publishIndexClaim(ctx context.Context, id ucan.Issuer, blobIndex blobindexlookup.BlobIndexLookup, claims contentclaims.Service, provIndex providerindex.ProviderIndex, provider peer.AddrInfo, claim ucan.Invocation, claimMeta ucan.Container) error { + var args assert.IndexArguments + if err := args.UnmarshalCBOR(bytes.NewReader(claim.ArgumentsBytes())); err != nil { + return fmt.Errorf("unmarshaling index claim arguments: %w", err) + } + + err := claims.Publish(ctx, claim) + if err != nil { + return fmt.Errorf("caching index claim with claim lookup: %w", err) + } + + results, err := provIndex.Find(ctx, providerindex.QueryKey{ + Hash: args.Index.Hash(), + TargetClaims: []multicodec.Code{metadata.LocationCommitmentID}, + }) + if err != nil { + return fmt.Errorf("finding location commitment: %w", err) + } + if len(results) == 0 { + return fmt.Errorf("no location commitments found for index: %s", args.Index) + } + + var idx blobindex.ShardedDagIndex + var ferr error + for _, r := range results { + idx, ferr = fetchBlobIndex(ctx, id, blobIndex, claims, args.Index, r, claim, claimMeta) + if ferr != nil { + continue + } + break + } + if ferr != nil { + return fmt.Errorf("fetching blob index: %w", ferr) + } + + var exp ucan.UnixTimestamp + if claim.Expiration() != nil { + exp = *claim.Expiration() + } + + meta := metadata.MetadataContext.New( + &metadata.IndexClaimMetadata{ + Index: args.Index, + Expiration: int64(exp), + Claim: claim.Link(), + }, + ) + + digests := bytemap.NewByteMap[multihash.Multihash, struct{}](-1) + for _, slices := range idx.Shards().Iterator() { + for d := range slices.Iterator() { + digests.Set(d, struct{}{}) + } + } + + contextID := args.Index.Bytes() + err = provIndex.Publish(ctx, provider, string(contextID), digests.Keys(), meta) + if err != nil { + return fmt.Errorf("publishing index claim: %w", err) + } + + return nil +} + +func fetchBlobIndex( + ctx context.Context, + id ucan.Issuer, + blobIndex blobindexlookup.BlobIndexLookup, + claims contentclaims.Service, + blobLink cid.Cid, + result model.ProviderResult, + cause ucan.Invocation, // supporting context (typically `/assert/index`) + causeMeta ucan.Container, +) (blobindex.ShardedDagIndex, error) { + meta := metadata.MetadataContext.New() + err := meta.UnmarshalBinary(result.Metadata) + if err != nil { + return nil, fmt.Errorf("decoding location commitment metadata: %w", err) + } + + protocol := meta.Get(metadata.LocationCommitmentID) + lcmeta, ok := protocol.(*metadata.LocationCommitmentMetadata) + if !ok { + return nil, errors.New("metadata is not expected type") + } + + if lcmeta.Shard != nil { + blobLink = *lcmeta.Shard + } + + blobURL, err := fetchRetrievalURL(*result.Provider, blobLink) + if err != nil { + return nil, fmt.Errorf("building retrieval URL: %w", err) + } + + aud, err := peerToPrincipal(result.Provider.ID) + if err != nil { + return nil, fmt.Errorf("converting provider peer ID to UCAN principal: %w", err) + } + + var wg sync.WaitGroup + wg.Add(1) + + var validateErr error + go func() { + defer wg.Done() + claimURL, err := fetchClaimURL(*result.Provider, lcmeta.Claim) + if err != nil { + validateErr = fmt.Errorf("building claim URL: %w", err) + return + } + + claim, err := claims.Find(ctx, lcmeta.Claim, claimURL) + if err != nil { + validateErr = err + return + } + + err = validateLocationCommitment(ctx, claim) + if err != nil { + validateErr = err + return + } + }() + + // Try to extract a /content/retrieve delegation from the /assert/index + // invocation. + proofs, err := extractContentRetrieveDelegation(ctx, id.DID(), cause, causeMeta) + if err != nil { + return nil, err + } + + byteRange := lcmeta.Range + if byteRange == nil || byteRange.Length == nil { + return nil, fmt.Errorf("missing byte range in location commitment metadata for index: %s", blobLink) + } + contentRange := content.Range{ + Start: byteRange.Offset, + End: byteRange.Offset + *byteRange.Length - 1, + } + req := types.RetrievalRequest{ + URL: *blobURL, + Range: &contentRange, + Auth: types.RetrievalAuth{ + Issuer: id, + Audience: aud, + Command: content.Retrieve.Command, + Subject: proofs[0].Subject(), + Arguments: &content.RetrieveArguments{ + Blob: content.Blob{Digest: blobLink.Hash()}, + Range: contentRange, + }, + Proofs: proofs, + }, + } + // Note: the ContextID here is of a location commitment provider + idx, err := blobIndex.Find(ctx, result.ContextID, result, req) + if err != nil { + return nil, fmt.Errorf("fetching index: %w", err) + } + + wg.Wait() + if validateErr != nil { + return nil, fmt.Errorf("verifying claim: %w", validateErr) + } + + return idx, nil +} + +// validateLocationCommitment ensures that the claim is a valid UCAN (signed, +// not expired etc.) and is a location commitment. +func validateLocationCommitment(ctx context.Context, claim ucan.Invocation) error { + if claim.Command() != assert.Location.Command { + return fmt.Errorf("unexpected command: %s", claim.Command()) + } + return validator.ValidateInvocation(ctx, claim) +} + +// peerToPrincipal converts a peer ID into a `did:key:`. Currently +// supports only ed25519 keys. +func peerToPrincipal(peer peer.ID) (did.DID, error) { + pk, err := peer.ExtractPublicKey() + if err != nil { + return did.Undef, fmt.Errorf("extracting public key from peer ID: %w", err) + } + pubBytes, err := pk.Raw() + if err != nil { + return did.Undef, fmt.Errorf("extracting raw bytes of public key: %w", err) + } + v, err := ed25519_verifier.FromRaw(pubBytes) + if err != nil { + return did.Undef, fmt.Errorf("decoding raw ed25519 public key: %w", err) + } + return v.KeyDID(), nil +} + +// extractContentRetrieveDelegation extracts a `/content/retrieve` +// delegation attached to the passed invocation (typically an `/assert/index`). +// The delegations are expected to be linked from metadata by a "retrievalAuth" +// key. +func extractContentRetrieveDelegation(ctx context.Context, audience did.DID, assertion ucan.Invocation, assertionMeta ucan.Container) ([]ucan.Delegation, error) { + var meta assert.IndexMetadata + if err := meta.UnmarshalCBOR(bytes.NewReader(assertion.MetadataBytes())); err != nil { + return nil, fmt.Errorf("unmarshaling assertion metadata: %w", err) + } + + if len(meta.RetrievalAuth) == 0 { + return nil, fmt.Errorf("missing content retrieval authorization") + } + + dlgs := map[cid.Cid]ucan.Delegation{} + for _, d := range assertionMeta.Delegations() { + dlgs[d.Link()] = d + } + + rootDelegation, ok := dlgs[meta.RetrievalAuth[0]] + if !ok { + return nil, fmt.Errorf("root retrieval auth delegation not found in index assertion metadata delegations: %s", meta.RetrievalAuth[0]) + } + + proofStore := ucanlib.NewContainerProofStore(assertionMeta) + proofs, _, err := proofStore.ProofChain(ctx, audience, content.Retrieve.Command, rootDelegation.Subject()) + if err != nil { + return nil, fmt.Errorf("building proof chain: %w", err) + } + + // ProofChain yields zero delegations (with no error) when no chain + // matches; downstream callers index proofs[0] unconditionally, so guard + // here to return an error instead of letting them panic. + if len(proofs) == 0 { + return nil, fmt.Errorf("no /content/retrieve proof chain found for audience %s subject %s (root retrieval auth: %s)", audience, rootDelegation.Subject(), meta.RetrievalAuth[0]) + } + + return proofs, nil +} diff --git a/indexing-service/pkg/service/service_test.go b/indexing-service/pkg/service/service_test.go new file mode 100644 index 00000000..a0c94391 --- /dev/null +++ b/indexing-service/pkg/service/service_test.go @@ -0,0 +1,117 @@ +package service + +import ( + "net/url" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/testutil" + "github.com/libp2p/go-libp2p/core/peer" + ma "github.com/multiformats/go-multiaddr" + "github.com/stretchr/testify/require" +) + +// TODO: TestQuery, TestPublishIndexClaim, TestPublishEqualsClaim, TestCacheClaim +// — the original suites exercised the legacy go-ucanto delegation flow and are +// being rewritten against the ucantone invocation flow. + +func TestUrlForResource(t *testing.T) { + const addrBase = "/dns/forge.fil.one/https/http-path/" + testCases := []struct { + name string + addrs []ma.Multiaddr + expectedUrl string + expectErr bool + replacements []replacement + }{ + { + name: "happy path", + addrs: []ma.Multiaddr{ + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims/{claim}")))(t), + }, + expectedUrl: "https://forge.fil.one/claims/123", + replacements: []replacement{ + {resourcePlaceholder: "{claim}", resourceID: "123"}, + }, + }, + { + name: "multiple addresses, uses the first one that contains the placeholder", + addrs: []ma.Multiaddr{ + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/blobs/{blob}")))(t), + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims1/{claim}")))(t), + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims2/{claim}")))(t), + }, + expectedUrl: "https://forge.fil.one/claims1/123", + replacements: []replacement{ + {resourcePlaceholder: "{claim}", resourceID: "123"}, + }, + }, + { + name: "no addresses in peer addr info", + addrs: []ma.Multiaddr{}, + expectErr: true, + replacements: []replacement{ + {resourcePlaceholder: "{claim}", resourceID: ""}, + }, + }, + { + name: "no address contains the placeholder", + addrs: []ma.Multiaddr{ + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims/{foo}")))(t), + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims/{bar}")))(t), + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims/{baz}")))(t), + }, + expectErr: true, + replacements: []replacement{ + {resourcePlaceholder: "{claim}", resourceID: ""}, + }, + }, + { + name: "happy path, multiple replacements", + addrs: []ma.Multiaddr{ + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims/{claim}-{subclaim}")))(t), + }, + expectedUrl: "https://forge.fil.one/claims/123-456", + replacements: []replacement{ + {resourcePlaceholder: "{claim}", resourceID: "123"}, + {resourcePlaceholder: "{subclaim}", resourceID: "456"}, + }, + }, + { + name: "multiple replacements, only one in source string", + addrs: []ma.Multiaddr{ + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims/{subclaim}")))(t), + }, + expectedUrl: "https://forge.fil.one/claims/456", + replacements: []replacement{ + {resourcePlaceholder: "{claim}", resourceID: "123"}, + {resourcePlaceholder: "{subclaim}", resourceID: "456"}, + }, + }, + { + name: "multiple addresses, multiple replacements, uses the first one that contains any the placeholder", + addrs: []ma.Multiaddr{ + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/blobs/{blob}")))(t), + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims1/{claim}")))(t), + testutil.Must(ma.NewMultiaddr(addrBase + url.PathEscape("/claims2/{claim}-{subclaim}")))(t), + }, + expectedUrl: "https://forge.fil.one/claims1/123", + replacements: []replacement{ + {resourcePlaceholder: "{claim}", resourceID: "123"}, + {resourcePlaceholder: "{subclaim}", resourceID: "456"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provider := peer.AddrInfo{Addrs: tc.addrs} + u, err := urlForResource(provider, tc.replacements) + if tc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedUrl, u.String()) + } + }) + } +} diff --git a/indexing-service/pkg/telemetry/client.go b/indexing-service/pkg/telemetry/client.go new file mode 100644 index 00000000..b8f2a588 --- /dev/null +++ b/indexing-service/pkg/telemetry/client.go @@ -0,0 +1,40 @@ +package telemetry + +import ( + "context" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + sdkresource "go.opentelemetry.io/otel/sdk/resource" + tracesdk "go.opentelemetry.io/otel/sdk/trace" +) + +// SetupClientTelemetry installs a minimal tracer provider for client-side usage so spans have valid IDs +// and trace context can be propagated. It uses an OTLP/HTTP exporter if OTEL_EXPORTER_OTLP_ENDPOINT is set; +// otherwise it runs with no exporter (noop span export) to avoid failing in CLI contexts. +func SetupClientTelemetry(ctx context.Context) (func(context.Context) error, error) { + var opts []tracesdk.TracerProviderOption + + // default to AlwaysSample for client spans so manual calls can create traces even without a parent + opts = append(opts, tracesdk.WithSampler(tracesdk.AlwaysSample())) + + // try to create an exporter if an endpoint is configured; otherwise fall back to no exporter + exp, err := otlptracehttp.New(ctx) + if err == nil { + opts = append(opts, tracesdk.WithBatcher(exp)) + } + + // even without resource attributes, ensure we have a valid provider + opts = append(opts, tracesdk.WithResource(sdkresource.Empty())) + + tp := tracesdk.NewTracerProvider(opts...) + + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + return tp.Shutdown, nil +} diff --git a/indexing-service/pkg/telemetry/otel.go b/indexing-service/pkg/telemetry/otel.go new file mode 100644 index 00000000..ac7a33b7 --- /dev/null +++ b/indexing-service/pkg/telemetry/otel.go @@ -0,0 +1,145 @@ +package telemetry + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/redis/go-redis/extra/redisotel/v9" + "github.com/redis/go-redis/v9" + ecsdetector "go.opentelemetry.io/contrib/detectors/aws/ecs" + "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-lambda-go/otellambda" + "go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + tracesdk "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/trace" +) + +type config struct { + baseSampler tracesdk.Sampler +} + +type TelemetryOption func(*config) error + +func WithBaseSampler(baseSampler tracesdk.Sampler) TelemetryOption { + return func(c *config) error { + c.baseSampler = baseSampler + return nil + } +} + +// SetupTelemetry configures the OpenTelemetry SDK by setting up a global tracer provider. +// It also adds instrumentation middleware to the config so that all AWS SDK clients based on that config are instrumented. +// This function updates the configuration in place. It should be called before any AWS SDK clients are created. +func SetupTelemetry(ctx context.Context, cfg *aws.Config, opts ...TelemetryOption) (func(context.Context), error) { + c := config{ + // Default to only tracing when there is an incoming sampled parent (e.g. upstream service already tracing). + // This avoids generating root spans for ad-hoc Lambda invocations that arrive without trace headers. + baseSampler: tracesdk.NeverSample(), + } + for _, opt := range opts { + if err := opt(&c); err != nil { + return nil, err + } + } + + // The default HTTP exporter will fetch the endpoint from the OTEL_EXPORTER_OTLP_ENDPOINT environment variable + // and will use the headers from the OTEL_EXPORTER_OTLP_HEADERS environment variable + exp, err := otlptracehttp.New(ctx) + if err != nil { + return nil, err + } + + // the resource detector populates some span attributes with information about the environment + detector := ecsdetector.NewResourceDetector() + resource, err := detector.Detect(ctx) + if err != nil { + return nil, err + } + + // accept incoming trace context from upstream services in both W3C Trace Context and Baggage formats + prop := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) + otel.SetTextMapPropagator(prop) + + // Use ParentBased on the configured base sampler. With the default NeverSample base sampler, we emit spans only + // when the incoming request already has a sampled parent trace. + tp := tracesdk.NewTracerProvider( + tracesdk.WithSampler(tracesdk.ParentBased(c.baseSampler)), + tracesdk.WithBatcher(exp), + tracesdk.WithResource(resource), + ) + + shutdownFunc := func(ctx context.Context) { + err := tp.Shutdown(ctx) + if err != nil { + fmt.Printf("error shutting down tracer provider: %v", err) + } + } + + // set as the global tracer provider. It is common for OpenTelemetry libraries to use the global tracer provider + // as the default provider if one is not provided + otel.SetTracerProvider(tp) + + // instrument all aws clients + otelaws.AppendMiddlewares(&cfg.APIOptions) + + return shutdownFunc, nil +} + +func InstrumentLambdaHandler(handlerFunc interface{}) interface{} { + tp := otel.GetTracerProvider() + asFlusher := tp.(otellambda.Flusher) + + return otellambda.InstrumentHandler( + handlerFunc, + otellambda.WithEventToCarrier(jsonEventHeadersToCarrier), + otellambda.WithTracerProvider(tp), + otellambda.WithFlusher(asFlusher), + ) +} + +// jsonEventHeadersToCarrier returns a TextMapCarrier that extracts its data from the passed JSON event. It will look +// for the "headers" field in the event, which is populated by the AWS API Gateway. Therefore, this function will make +// distributed tracing work only with HTTP-based lambdas that are triggered by the API Gateway. +func jsonEventHeadersToCarrier(eventJSON []byte) propagation.TextMapCarrier { + var apiGatewayEvent struct { + Headers map[string]string `json:"headers"` + } + + if err := json.Unmarshal(eventJSON, &apiGatewayEvent); err != nil { + return propagation.MapCarrier{} + } + + return propagation.MapCarrier(apiGatewayEvent.Headers) +} + +func InstrumentHTTPClient(client *http.Client) *http.Client { + instrumentedTransport := otelhttp.NewTransport(client.Transport) + client.Transport = instrumentedTransport + + return client +} + +func InstrumentRedisClient(client *redis.ClusterClient) *redis.ClusterClient { + redisotel.InstrumentTracing(client) + return client +} + +func StartSpan(ctx context.Context, name string) (context.Context, trace.Span) { + t := otel.Tracer("") + return t.Start(ctx, name) +} + +func Error(span trace.Span, err error, msg string) { + span.SetStatus(codes.Error, msg) + span.RecordError(err) +} diff --git a/indexing-service/pkg/telemetry/sentry.go b/indexing-service/pkg/telemetry/sentry.go new file mode 100644 index 00000000..aeb969ce --- /dev/null +++ b/indexing-service/pkg/telemetry/sentry.go @@ -0,0 +1,112 @@ +package telemetry + +import ( + "fmt" + "strings" + + "github.com/getsentry/sentry-go" + logging "github.com/ipfs/go-log/v2" +) + +type SentryExceptionCaptureFunc func(err error) *sentry.EventID + +// SentryLogger is a logger that sends errors messages to Sentry for error, +// panic and fatal logs. +type SentryLogger struct { + system string + log logging.EventLogger + captureException SentryExceptionCaptureFunc +} + +func (s *SentryLogger) Debug(args ...any) { + s.log.Debug(args...) +} + +func (s *SentryLogger) Debugf(format string, args ...any) { + s.log.Debugf(format, args...) +} + +func (s *SentryLogger) Error(args ...any) { + if getLevel(s.system) <= logging.LevelError { + s.captureException(fmt.Errorf(formatString(len(args)), args...)) + } + s.log.Error(args...) +} + +func (s *SentryLogger) Errorf(format string, args ...any) { + if getLevel(s.system) <= logging.LevelError { + s.captureException(fmt.Errorf(format, args...)) + } + s.log.Errorf(format, args...) +} + +func (s *SentryLogger) Fatal(args ...any) { + if getLevel(s.system) <= logging.LevelFatal { + s.captureException(fmt.Errorf(formatString(len(args)), args...)) + } + s.log.Fatal(args...) +} + +func (s *SentryLogger) Fatalf(format string, args ...any) { + if getLevel(s.system) <= logging.LevelFatal { + s.captureException(fmt.Errorf(format, args...)) + } + s.log.Fatalf(format, args...) +} + +func (s *SentryLogger) Info(args ...any) { + s.log.Info(args...) +} + +func (s *SentryLogger) Infof(format string, args ...any) { + s.log.Infof(format, args...) +} + +func (s *SentryLogger) Panic(args ...any) { + if getLevel(s.system) <= logging.LevelPanic { + s.captureException(fmt.Errorf(formatString(len(args)), args...)) + } + s.log.Panic(args...) +} + +func (s *SentryLogger) Panicf(format string, args ...any) { + if getLevel(s.system) <= logging.LevelPanic { + s.captureException(fmt.Errorf(format, args...)) + } + s.log.Panicf(format, args...) +} + +func (s *SentryLogger) Warn(args ...any) { + s.log.Warn(args...) +} + +func (s *SentryLogger) Warnf(format string, args ...any) { + s.log.Warnf(format, args...) +} + +// NewSentryLogger returns a logger that sends errors messages to Sentry for +// error, panic and fatal logs. +// +// Note: you should call [sentry.Init] before using the returned logger. +func NewSentryLogger(system string) *SentryLogger { + return &SentryLogger{ + system: system, + log: logging.Logger(system), + captureException: sentry.CaptureException, + } +} + +// formatString gets a format string for the specified number of arguments. +func formatString(n int) string { + return strings.Repeat(" %+v", n)[1:] +} + +// getLevel gets the configured log level for the passed subsystem. +func getLevel(system string) logging.LogLevel { + cfg := logging.GetConfig() + lvl, ok := cfg.SubsystemLevels[system] + if !ok { + return cfg.Level + } + return lvl +} diff --git a/indexing-service/pkg/telemetry/sentry_test.go b/indexing-service/pkg/telemetry/sentry_test.go new file mode 100644 index 00000000..a09bafe4 --- /dev/null +++ b/indexing-service/pkg/telemetry/sentry_test.go @@ -0,0 +1,173 @@ +package telemetry + +import ( + "fmt" + "reflect" + "testing" + + "github.com/fil-forge/forge/indexing-service/pkg/internal/extmocks" + "github.com/getsentry/sentry-go" + logging "github.com/ipfs/go-log/v2" + "github.com/stretchr/testify/mock" +) + +type MockSentry struct { + mock.Mock +} + +func (m *MockSentry) CaptureException(err error) *sentry.EventID { + args := m.Called(err) + id := sentry.EventID(args.String(0)) + return &id +} + +func TestSentryLogger(t *testing.T) { + testCases := []struct { + method string + format string + args []any + capture bool + level logging.LogLevel + }{ + { + method: "Debug", + args: []any{"test"}, + }, + { + method: "Debugf", + format: "test %s", + args: []any{"arg"}, + }, + { + method: "Info", + args: []any{"test"}, + }, + { + method: "Infof", + format: "test %s", + args: []any{"arg"}, + }, + { + method: "Warn", + args: []any{"test"}, + }, + { + method: "Warnf", + format: "test %s", + args: []any{"arg"}, + }, + { + method: "Error", + args: []any{"boom"}, + capture: true, + }, + { + method: "Error", + args: []any{"boom"}, + capture: false, + level: logging.LevelPanic, + }, + { + method: "Errorf", + format: "boom %s", + args: []any{"arg"}, + capture: true, + }, + { + method: "Errorf", + format: "boom %s", + args: []any{"arg"}, + capture: false, + level: logging.LevelPanic, + }, + { + method: "Panic", + args: []any{"boom"}, + capture: true, + }, + { + method: "Panicf", + format: "boom %s", + args: []any{"arg"}, + capture: true, + }, + { + method: "Panic", + args: []any{"boom"}, + capture: false, + level: logging.LevelFatal, + }, + { + method: "Panicf", + format: "boom %s", + args: []any{"arg"}, + capture: false, + level: logging.LevelFatal, + }, + { + method: "Fatal", + args: []any{"boom"}, + capture: true, + }, + { + method: "Fatalf", + format: "boom %s", + args: []any{"arg"}, + capture: true, + }, + { + method: "Fatal", + args: []any{"boom"}, + capture: false, + level: logging.LogLevel(99), + }, + { + method: "Fatalf", + format: "boom %s", + args: []any{"arg"}, + capture: false, + level: logging.LogLevel(99), + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s capture=%t level=%d", tc.method, tc.capture, tc.level), func(t *testing.T) { + mockSentry := MockSentry{} + mockLog := extmocks.NewMockEventLogger(t) + system := fmt.Sprintf("test-%s", tc.method) + log := SentryLogger{ + system: system, + log: mockLog, + captureException: mockSentry.CaptureException, + } + + cfg := logging.GetConfig() + cfg.SubsystemLevels[system] = tc.level + logging.SetupLogging(cfg) + + var args []any + if tc.format != "" { + args = append(args, tc.format) + } + args = append(args, tc.args...) + + mockLog.On(tc.method, args...).Return() + if tc.capture { + if tc.format == "" { + mockSentry.On("CaptureException", fmt.Errorf(formatString(len(tc.args)), tc.args...)).Return("eventID") + } else { + mockSentry.On("CaptureException", fmt.Errorf(tc.format, tc.args...)).Return("eventID") + } + } + + vals := make([]reflect.Value, len(args)) + for i, a := range args { + vals[i] = reflect.ValueOf(a) + } + reflect.ValueOf(&log).MethodByName(tc.method).Call(vals) + + mockSentry.AssertExpectations(t) + mockLog.AssertExpectations(t) + }) + } +} diff --git a/indexing-service/pkg/types/mock_ContentClaimsCache.go b/indexing-service/pkg/types/mock_ContentClaimsCache.go new file mode 100644 index 00000000..95d35af8 --- /dev/null +++ b/indexing-service/pkg/types/mock_ContentClaimsCache.go @@ -0,0 +1,240 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package types + +import ( + "context" + + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + mock "github.com/stretchr/testify/mock" +) + +// NewMockContentClaimsCache creates a new instance of MockContentClaimsCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockContentClaimsCache(t interface { + mock.TestingT + Cleanup(func()) +}) *MockContentClaimsCache { + mock := &MockContentClaimsCache{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockContentClaimsCache is an autogenerated mock type for the ContentClaimsCache type +type MockContentClaimsCache struct { + mock.Mock +} + +type MockContentClaimsCache_Expecter struct { + mock *mock.Mock +} + +func (_m *MockContentClaimsCache) EXPECT() *MockContentClaimsCache_Expecter { + return &MockContentClaimsCache_Expecter{mock: &_m.Mock} +} + +// Get provides a mock function for the type MockContentClaimsCache +func (_mock *MockContentClaimsCache) Get(ctx context.Context, key cid.Cid) (ucan.Invocation, error) { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 ucan.Invocation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid) (ucan.Invocation, error)); ok { + return returnFunc(ctx, key) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid) ucan.Invocation); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ucan.Invocation) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, cid.Cid) error); ok { + r1 = returnFunc(ctx, key) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockContentClaimsCache_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockContentClaimsCache_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key cid.Cid +func (_e *MockContentClaimsCache_Expecter) Get(ctx interface{}, key interface{}) *MockContentClaimsCache_Get_Call { + return &MockContentClaimsCache_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *MockContentClaimsCache_Get_Call) Run(run func(ctx context.Context, key cid.Cid)) *MockContentClaimsCache_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 cid.Cid + if args[1] != nil { + arg1 = args[1].(cid.Cid) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockContentClaimsCache_Get_Call) Return(invocation ucan.Invocation, err error) *MockContentClaimsCache_Get_Call { + _c.Call.Return(invocation, err) + return _c +} + +func (_c *MockContentClaimsCache_Get_Call) RunAndReturn(run func(ctx context.Context, key cid.Cid) (ucan.Invocation, error)) *MockContentClaimsCache_Get_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type MockContentClaimsCache +func (_mock *MockContentClaimsCache) Set(ctx context.Context, key cid.Cid, value ucan.Invocation, expires bool) error { + ret := _mock.Called(ctx, key, value, expires) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid, ucan.Invocation, bool) error); ok { + r0 = returnFunc(ctx, key, value, expires) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockContentClaimsCache_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type MockContentClaimsCache_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - ctx context.Context +// - key cid.Cid +// - value ucan.Invocation +// - expires bool +func (_e *MockContentClaimsCache_Expecter) Set(ctx interface{}, key interface{}, value interface{}, expires interface{}) *MockContentClaimsCache_Set_Call { + return &MockContentClaimsCache_Set_Call{Call: _e.mock.On("Set", ctx, key, value, expires)} +} + +func (_c *MockContentClaimsCache_Set_Call) Run(run func(ctx context.Context, key cid.Cid, value ucan.Invocation, expires bool)) *MockContentClaimsCache_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 cid.Cid + if args[1] != nil { + arg1 = args[1].(cid.Cid) + } + var arg2 ucan.Invocation + if args[2] != nil { + arg2 = args[2].(ucan.Invocation) + } + var arg3 bool + if args[3] != nil { + arg3 = args[3].(bool) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockContentClaimsCache_Set_Call) Return(err error) *MockContentClaimsCache_Set_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockContentClaimsCache_Set_Call) RunAndReturn(run func(ctx context.Context, key cid.Cid, value ucan.Invocation, expires bool) error) *MockContentClaimsCache_Set_Call { + _c.Call.Return(run) + return _c +} + +// SetExpirable provides a mock function for the type MockContentClaimsCache +func (_mock *MockContentClaimsCache) SetExpirable(ctx context.Context, key cid.Cid, expires bool) error { + ret := _mock.Called(ctx, key, expires) + + if len(ret) == 0 { + panic("no return value specified for SetExpirable") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid, bool) error); ok { + r0 = returnFunc(ctx, key, expires) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockContentClaimsCache_SetExpirable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetExpirable' +type MockContentClaimsCache_SetExpirable_Call struct { + *mock.Call +} + +// SetExpirable is a helper method to define mock.On call +// - ctx context.Context +// - key cid.Cid +// - expires bool +func (_e *MockContentClaimsCache_Expecter) SetExpirable(ctx interface{}, key interface{}, expires interface{}) *MockContentClaimsCache_SetExpirable_Call { + return &MockContentClaimsCache_SetExpirable_Call{Call: _e.mock.On("SetExpirable", ctx, key, expires)} +} + +func (_c *MockContentClaimsCache_SetExpirable_Call) Run(run func(ctx context.Context, key cid.Cid, expires bool)) *MockContentClaimsCache_SetExpirable_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 cid.Cid + if args[1] != nil { + arg1 = args[1].(cid.Cid) + } + var arg2 bool + if args[2] != nil { + arg2 = args[2].(bool) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockContentClaimsCache_SetExpirable_Call) Return(err error) *MockContentClaimsCache_SetExpirable_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockContentClaimsCache_SetExpirable_Call) RunAndReturn(run func(ctx context.Context, key cid.Cid, expires bool) error) *MockContentClaimsCache_SetExpirable_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/types/mock_ContentClaimsStore.go b/indexing-service/pkg/types/mock_ContentClaimsStore.go new file mode 100644 index 00000000..6842fe0c --- /dev/null +++ b/indexing-service/pkg/types/mock_ContentClaimsStore.go @@ -0,0 +1,171 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package types + +import ( + "context" + + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + mock "github.com/stretchr/testify/mock" +) + +// NewMockContentClaimsStore creates a new instance of MockContentClaimsStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockContentClaimsStore(t interface { + mock.TestingT + Cleanup(func()) +}) *MockContentClaimsStore { + mock := &MockContentClaimsStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockContentClaimsStore is an autogenerated mock type for the ContentClaimsStore type +type MockContentClaimsStore struct { + mock.Mock +} + +type MockContentClaimsStore_Expecter struct { + mock *mock.Mock +} + +func (_m *MockContentClaimsStore) EXPECT() *MockContentClaimsStore_Expecter { + return &MockContentClaimsStore_Expecter{mock: &_m.Mock} +} + +// Get provides a mock function for the type MockContentClaimsStore +func (_mock *MockContentClaimsStore) Get(ctx context.Context, key cid.Cid) (ucan.Invocation, error) { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 ucan.Invocation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid) (ucan.Invocation, error)); ok { + return returnFunc(ctx, key) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid) ucan.Invocation); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ucan.Invocation) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, cid.Cid) error); ok { + r1 = returnFunc(ctx, key) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockContentClaimsStore_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockContentClaimsStore_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key cid.Cid +func (_e *MockContentClaimsStore_Expecter) Get(ctx interface{}, key interface{}) *MockContentClaimsStore_Get_Call { + return &MockContentClaimsStore_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *MockContentClaimsStore_Get_Call) Run(run func(ctx context.Context, key cid.Cid)) *MockContentClaimsStore_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 cid.Cid + if args[1] != nil { + arg1 = args[1].(cid.Cid) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockContentClaimsStore_Get_Call) Return(invocation ucan.Invocation, err error) *MockContentClaimsStore_Get_Call { + _c.Call.Return(invocation, err) + return _c +} + +func (_c *MockContentClaimsStore_Get_Call) RunAndReturn(run func(ctx context.Context, key cid.Cid) (ucan.Invocation, error)) *MockContentClaimsStore_Get_Call { + _c.Call.Return(run) + return _c +} + +// Put provides a mock function for the type MockContentClaimsStore +func (_mock *MockContentClaimsStore) Put(ctx context.Context, key cid.Cid, value ucan.Invocation) error { + ret := _mock.Called(ctx, key, value) + + if len(ret) == 0 { + panic("no return value specified for Put") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid, ucan.Invocation) error); ok { + r0 = returnFunc(ctx, key, value) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockContentClaimsStore_Put_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Put' +type MockContentClaimsStore_Put_Call struct { + *mock.Call +} + +// Put is a helper method to define mock.On call +// - ctx context.Context +// - key cid.Cid +// - value ucan.Invocation +func (_e *MockContentClaimsStore_Expecter) Put(ctx interface{}, key interface{}, value interface{}) *MockContentClaimsStore_Put_Call { + return &MockContentClaimsStore_Put_Call{Call: _e.mock.On("Put", ctx, key, value)} +} + +func (_c *MockContentClaimsStore_Put_Call) Run(run func(ctx context.Context, key cid.Cid, value ucan.Invocation)) *MockContentClaimsStore_Put_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 cid.Cid + if args[1] != nil { + arg1 = args[1].(cid.Cid) + } + var arg2 ucan.Invocation + if args[2] != nil { + arg2 = args[2].(ucan.Invocation) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockContentClaimsStore_Put_Call) Return(err error) *MockContentClaimsStore_Put_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockContentClaimsStore_Put_Call) RunAndReturn(run func(ctx context.Context, key cid.Cid, value ucan.Invocation) error) *MockContentClaimsStore_Put_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/types/mock_NoProviderStore.go b/indexing-service/pkg/types/mock_NoProviderStore.go new file mode 100644 index 00000000..77f5a203 --- /dev/null +++ b/indexing-service/pkg/types/mock_NoProviderStore.go @@ -0,0 +1,256 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package types + +import ( + "context" + + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" + mock "github.com/stretchr/testify/mock" +) + +// NewMockNoProviderStore creates a new instance of MockNoProviderStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockNoProviderStore(t interface { + mock.TestingT + Cleanup(func()) +}) *MockNoProviderStore { + mock := &MockNoProviderStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockNoProviderStore is an autogenerated mock type for the NoProviderStore type +type MockNoProviderStore struct { + mock.Mock +} + +type MockNoProviderStore_Expecter struct { + mock *mock.Mock +} + +func (_m *MockNoProviderStore) EXPECT() *MockNoProviderStore_Expecter { + return &MockNoProviderStore_Expecter{mock: &_m.Mock} +} + +// Add provides a mock function for the type MockNoProviderStore +func (_mock *MockNoProviderStore) Add(ctx context.Context, key multihash.Multihash, values ...multicodec.Code) (uint64, error) { + // multicodec.Code + _va := make([]interface{}, len(values)) + for _i := range values { + _va[_i] = values[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, key) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 uint64 + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash, ...multicodec.Code) (uint64, error)); ok { + return returnFunc(ctx, key, values...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash, ...multicodec.Code) uint64); ok { + r0 = returnFunc(ctx, key, values...) + } else { + r0 = ret.Get(0).(uint64) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, multihash.Multihash, ...multicodec.Code) error); ok { + r1 = returnFunc(ctx, key, values...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockNoProviderStore_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' +type MockNoProviderStore_Add_Call struct { + *mock.Call +} + +// Add is a helper method to define mock.On call +// - ctx context.Context +// - key multihash.Multihash +// - values ...multicodec.Code +func (_e *MockNoProviderStore_Expecter) Add(ctx interface{}, key interface{}, values ...interface{}) *MockNoProviderStore_Add_Call { + return &MockNoProviderStore_Add_Call{Call: _e.mock.On("Add", + append([]interface{}{ctx, key}, values...)...)} +} + +func (_c *MockNoProviderStore_Add_Call) Run(run func(ctx context.Context, key multihash.Multihash, values ...multicodec.Code)) *MockNoProviderStore_Add_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 multihash.Multihash + if args[1] != nil { + arg1 = args[1].(multihash.Multihash) + } + var arg2 []multicodec.Code + variadicArgs := make([]multicodec.Code, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(multicodec.Code) + } + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockNoProviderStore_Add_Call) Return(v uint64, err error) *MockNoProviderStore_Add_Call { + _c.Call.Return(v, err) + return _c +} + +func (_c *MockNoProviderStore_Add_Call) RunAndReturn(run func(ctx context.Context, key multihash.Multihash, values ...multicodec.Code) (uint64, error)) *MockNoProviderStore_Add_Call { + _c.Call.Return(run) + return _c +} + +// Members provides a mock function for the type MockNoProviderStore +func (_mock *MockNoProviderStore) Members(ctx context.Context, key multihash.Multihash) ([]multicodec.Code, error) { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Members") + } + + var r0 []multicodec.Code + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash) ([]multicodec.Code, error)); ok { + return returnFunc(ctx, key) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash) []multicodec.Code); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]multicodec.Code) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, multihash.Multihash) error); ok { + r1 = returnFunc(ctx, key) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockNoProviderStore_Members_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Members' +type MockNoProviderStore_Members_Call struct { + *mock.Call +} + +// Members is a helper method to define mock.On call +// - ctx context.Context +// - key multihash.Multihash +func (_e *MockNoProviderStore_Expecter) Members(ctx interface{}, key interface{}) *MockNoProviderStore_Members_Call { + return &MockNoProviderStore_Members_Call{Call: _e.mock.On("Members", ctx, key)} +} + +func (_c *MockNoProviderStore_Members_Call) Run(run func(ctx context.Context, key multihash.Multihash)) *MockNoProviderStore_Members_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 multihash.Multihash + if args[1] != nil { + arg1 = args[1].(multihash.Multihash) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockNoProviderStore_Members_Call) Return(codes []multicodec.Code, err error) *MockNoProviderStore_Members_Call { + _c.Call.Return(codes, err) + return _c +} + +func (_c *MockNoProviderStore_Members_Call) RunAndReturn(run func(ctx context.Context, key multihash.Multihash) ([]multicodec.Code, error)) *MockNoProviderStore_Members_Call { + _c.Call.Return(run) + return _c +} + +// SetExpirable provides a mock function for the type MockNoProviderStore +func (_mock *MockNoProviderStore) SetExpirable(ctx context.Context, key multihash.Multihash, expires bool) error { + ret := _mock.Called(ctx, key, expires) + + if len(ret) == 0 { + panic("no return value specified for SetExpirable") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash, bool) error); ok { + r0 = returnFunc(ctx, key, expires) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockNoProviderStore_SetExpirable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetExpirable' +type MockNoProviderStore_SetExpirable_Call struct { + *mock.Call +} + +// SetExpirable is a helper method to define mock.On call +// - ctx context.Context +// - key multihash.Multihash +// - expires bool +func (_e *MockNoProviderStore_Expecter) SetExpirable(ctx interface{}, key interface{}, expires interface{}) *MockNoProviderStore_SetExpirable_Call { + return &MockNoProviderStore_SetExpirable_Call{Call: _e.mock.On("SetExpirable", ctx, key, expires)} +} + +func (_c *MockNoProviderStore_SetExpirable_Call) Run(run func(ctx context.Context, key multihash.Multihash, expires bool)) *MockNoProviderStore_SetExpirable_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 multihash.Multihash + if args[1] != nil { + arg1 = args[1].(multihash.Multihash) + } + var arg2 bool + if args[2] != nil { + arg2 = args[2].(bool) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockNoProviderStore_SetExpirable_Call) Return(err error) *MockNoProviderStore_SetExpirable_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockNoProviderStore_SetExpirable_Call) RunAndReturn(run func(ctx context.Context, key multihash.Multihash, expires bool) error) *MockNoProviderStore_SetExpirable_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/types/mock_ProviderStore.go b/indexing-service/pkg/types/mock_ProviderStore.go new file mode 100644 index 00000000..209e18a9 --- /dev/null +++ b/indexing-service/pkg/types/mock_ProviderStore.go @@ -0,0 +1,302 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package types + +import ( + "context" + + "github.com/ipni/go-libipni/find/model" + "github.com/multiformats/go-multihash" + mock "github.com/stretchr/testify/mock" +) + +// NewMockProviderStore creates a new instance of MockProviderStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockProviderStore(t interface { + mock.TestingT + Cleanup(func()) +}) *MockProviderStore { + mock := &MockProviderStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockProviderStore is an autogenerated mock type for the ProviderStore type +type MockProviderStore struct { + mock.Mock +} + +type MockProviderStore_Expecter struct { + mock *mock.Mock +} + +func (_m *MockProviderStore) EXPECT() *MockProviderStore_Expecter { + return &MockProviderStore_Expecter{mock: &_m.Mock} +} + +// Add provides a mock function for the type MockProviderStore +func (_mock *MockProviderStore) Add(ctx context.Context, key multihash.Multihash, values ...model.ProviderResult) (uint64, error) { + // model.ProviderResult + _va := make([]interface{}, len(values)) + for _i := range values { + _va[_i] = values[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, key) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 uint64 + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash, ...model.ProviderResult) (uint64, error)); ok { + return returnFunc(ctx, key, values...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash, ...model.ProviderResult) uint64); ok { + r0 = returnFunc(ctx, key, values...) + } else { + r0 = ret.Get(0).(uint64) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, multihash.Multihash, ...model.ProviderResult) error); ok { + r1 = returnFunc(ctx, key, values...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockProviderStore_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' +type MockProviderStore_Add_Call struct { + *mock.Call +} + +// Add is a helper method to define mock.On call +// - ctx context.Context +// - key multihash.Multihash +// - values ...model.ProviderResult +func (_e *MockProviderStore_Expecter) Add(ctx interface{}, key interface{}, values ...interface{}) *MockProviderStore_Add_Call { + return &MockProviderStore_Add_Call{Call: _e.mock.On("Add", + append([]interface{}{ctx, key}, values...)...)} +} + +func (_c *MockProviderStore_Add_Call) Run(run func(ctx context.Context, key multihash.Multihash, values ...model.ProviderResult)) *MockProviderStore_Add_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 multihash.Multihash + if args[1] != nil { + arg1 = args[1].(multihash.Multihash) + } + var arg2 []model.ProviderResult + variadicArgs := make([]model.ProviderResult, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(model.ProviderResult) + } + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockProviderStore_Add_Call) Return(v uint64, err error) *MockProviderStore_Add_Call { + _c.Call.Return(v, err) + return _c +} + +func (_c *MockProviderStore_Add_Call) RunAndReturn(run func(ctx context.Context, key multihash.Multihash, values ...model.ProviderResult) (uint64, error)) *MockProviderStore_Add_Call { + _c.Call.Return(run) + return _c +} + +// Batch provides a mock function for the type MockProviderStore +func (_mock *MockProviderStore) Batch() ValueSetCacheBatcher[multihash.Multihash, model.ProviderResult] { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Batch") + } + + var r0 ValueSetCacheBatcher[multihash.Multihash, model.ProviderResult] + if returnFunc, ok := ret.Get(0).(func() ValueSetCacheBatcher[multihash.Multihash, model.ProviderResult]); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ValueSetCacheBatcher[multihash.Multihash, model.ProviderResult]) + } + } + return r0 +} + +// MockProviderStore_Batch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Batch' +type MockProviderStore_Batch_Call struct { + *mock.Call +} + +// Batch is a helper method to define mock.On call +func (_e *MockProviderStore_Expecter) Batch() *MockProviderStore_Batch_Call { + return &MockProviderStore_Batch_Call{Call: _e.mock.On("Batch")} +} + +func (_c *MockProviderStore_Batch_Call) Run(run func()) *MockProviderStore_Batch_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockProviderStore_Batch_Call) Return(valueSetCacheBatcher ValueSetCacheBatcher[multihash.Multihash, model.ProviderResult]) *MockProviderStore_Batch_Call { + _c.Call.Return(valueSetCacheBatcher) + return _c +} + +func (_c *MockProviderStore_Batch_Call) RunAndReturn(run func() ValueSetCacheBatcher[multihash.Multihash, model.ProviderResult]) *MockProviderStore_Batch_Call { + _c.Call.Return(run) + return _c +} + +// Members provides a mock function for the type MockProviderStore +func (_mock *MockProviderStore) Members(ctx context.Context, key multihash.Multihash) ([]model.ProviderResult, error) { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Members") + } + + var r0 []model.ProviderResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash) ([]model.ProviderResult, error)); ok { + return returnFunc(ctx, key) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash) []model.ProviderResult); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]model.ProviderResult) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, multihash.Multihash) error); ok { + r1 = returnFunc(ctx, key) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockProviderStore_Members_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Members' +type MockProviderStore_Members_Call struct { + *mock.Call +} + +// Members is a helper method to define mock.On call +// - ctx context.Context +// - key multihash.Multihash +func (_e *MockProviderStore_Expecter) Members(ctx interface{}, key interface{}) *MockProviderStore_Members_Call { + return &MockProviderStore_Members_Call{Call: _e.mock.On("Members", ctx, key)} +} + +func (_c *MockProviderStore_Members_Call) Run(run func(ctx context.Context, key multihash.Multihash)) *MockProviderStore_Members_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 multihash.Multihash + if args[1] != nil { + arg1 = args[1].(multihash.Multihash) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockProviderStore_Members_Call) Return(providerResults []model.ProviderResult, err error) *MockProviderStore_Members_Call { + _c.Call.Return(providerResults, err) + return _c +} + +func (_c *MockProviderStore_Members_Call) RunAndReturn(run func(ctx context.Context, key multihash.Multihash) ([]model.ProviderResult, error)) *MockProviderStore_Members_Call { + _c.Call.Return(run) + return _c +} + +// SetExpirable provides a mock function for the type MockProviderStore +func (_mock *MockProviderStore) SetExpirable(ctx context.Context, key multihash.Multihash, expires bool) error { + ret := _mock.Called(ctx, key, expires) + + if len(ret) == 0 { + panic("no return value specified for SetExpirable") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, multihash.Multihash, bool) error); ok { + r0 = returnFunc(ctx, key, expires) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockProviderStore_SetExpirable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetExpirable' +type MockProviderStore_SetExpirable_Call struct { + *mock.Call +} + +// SetExpirable is a helper method to define mock.On call +// - ctx context.Context +// - key multihash.Multihash +// - expires bool +func (_e *MockProviderStore_Expecter) SetExpirable(ctx interface{}, key interface{}, expires interface{}) *MockProviderStore_SetExpirable_Call { + return &MockProviderStore_SetExpirable_Call{Call: _e.mock.On("SetExpirable", ctx, key, expires)} +} + +func (_c *MockProviderStore_SetExpirable_Call) Run(run func(ctx context.Context, key multihash.Multihash, expires bool)) *MockProviderStore_SetExpirable_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 multihash.Multihash + if args[1] != nil { + arg1 = args[1].(multihash.Multihash) + } + var arg2 bool + if args[2] != nil { + arg2 = args[2].(bool) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockProviderStore_SetExpirable_Call) Return(err error) *MockProviderStore_SetExpirable_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockProviderStore_SetExpirable_Call) RunAndReturn(run func(ctx context.Context, key multihash.Multihash, expires bool) error) *MockProviderStore_SetExpirable_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/types/mock_Service.go b/indexing-service/pkg/types/mock_Service.go new file mode 100644 index 00000000..0f7adfe9 --- /dev/null +++ b/indexing-service/pkg/types/mock_Service.go @@ -0,0 +1,309 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package types + +import ( + "context" + + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + "github.com/libp2p/go-libp2p/core/peer" + mock "github.com/stretchr/testify/mock" +) + +// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockService(t interface { + mock.TestingT + Cleanup(func()) +}) *MockService { + mock := &MockService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockService is an autogenerated mock type for the Service type +type MockService struct { + mock.Mock +} + +type MockService_Expecter struct { + mock *mock.Mock +} + +func (_m *MockService) EXPECT() *MockService_Expecter { + return &MockService_Expecter{mock: &_m.Mock} +} + +// Cache provides a mock function for the type MockService +func (_mock *MockService) Cache(ctx context.Context, provider peer.AddrInfo, claim ucan.Invocation, meta ucan.Container) error { + ret := _mock.Called(ctx, provider, claim, meta) + + if len(ret) == 0 { + panic("no return value specified for Cache") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, peer.AddrInfo, ucan.Invocation, ucan.Container) error); ok { + r0 = returnFunc(ctx, provider, claim, meta) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_Cache_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Cache' +type MockService_Cache_Call struct { + *mock.Call +} + +// Cache is a helper method to define mock.On call +// - ctx context.Context +// - provider peer.AddrInfo +// - claim ucan.Invocation +// - meta ucan.Container +func (_e *MockService_Expecter) Cache(ctx interface{}, provider interface{}, claim interface{}, meta interface{}) *MockService_Cache_Call { + return &MockService_Cache_Call{Call: _e.mock.On("Cache", ctx, provider, claim, meta)} +} + +func (_c *MockService_Cache_Call) Run(run func(ctx context.Context, provider peer.AddrInfo, claim ucan.Invocation, meta ucan.Container)) *MockService_Cache_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 peer.AddrInfo + if args[1] != nil { + arg1 = args[1].(peer.AddrInfo) + } + var arg2 ucan.Invocation + if args[2] != nil { + arg2 = args[2].(ucan.Invocation) + } + var arg3 ucan.Container + if args[3] != nil { + arg3 = args[3].(ucan.Container) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockService_Cache_Call) Return(err error) *MockService_Cache_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_Cache_Call) RunAndReturn(run func(ctx context.Context, provider peer.AddrInfo, claim ucan.Invocation, meta ucan.Container) error) *MockService_Cache_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type MockService +func (_mock *MockService) Get(ctx context.Context, claim cid.Cid) (ucan.Invocation, error) { + ret := _mock.Called(ctx, claim) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 ucan.Invocation + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid) (ucan.Invocation, error)); ok { + return returnFunc(ctx, claim) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, cid.Cid) ucan.Invocation); ok { + r0 = returnFunc(ctx, claim) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ucan.Invocation) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, cid.Cid) error); ok { + r1 = returnFunc(ctx, claim) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockService_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockService_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - claim cid.Cid +func (_e *MockService_Expecter) Get(ctx interface{}, claim interface{}) *MockService_Get_Call { + return &MockService_Get_Call{Call: _e.mock.On("Get", ctx, claim)} +} + +func (_c *MockService_Get_Call) Run(run func(ctx context.Context, claim cid.Cid)) *MockService_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 cid.Cid + if args[1] != nil { + arg1 = args[1].(cid.Cid) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockService_Get_Call) Return(invocation ucan.Invocation, err error) *MockService_Get_Call { + _c.Call.Return(invocation, err) + return _c +} + +func (_c *MockService_Get_Call) RunAndReturn(run func(ctx context.Context, claim cid.Cid) (ucan.Invocation, error)) *MockService_Get_Call { + _c.Call.Return(run) + return _c +} + +// Publish provides a mock function for the type MockService +func (_mock *MockService) Publish(ctx context.Context, claim ucan.Invocation, meta ucan.Container) error { + ret := _mock.Called(ctx, claim, meta) + + if len(ret) == 0 { + panic("no return value specified for Publish") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ucan.Invocation, ucan.Container) error); ok { + r0 = returnFunc(ctx, claim, meta) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockService_Publish_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Publish' +type MockService_Publish_Call struct { + *mock.Call +} + +// Publish is a helper method to define mock.On call +// - ctx context.Context +// - claim ucan.Invocation +// - meta ucan.Container +func (_e *MockService_Expecter) Publish(ctx interface{}, claim interface{}, meta interface{}) *MockService_Publish_Call { + return &MockService_Publish_Call{Call: _e.mock.On("Publish", ctx, claim, meta)} +} + +func (_c *MockService_Publish_Call) Run(run func(ctx context.Context, claim ucan.Invocation, meta ucan.Container)) *MockService_Publish_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 ucan.Invocation + if args[1] != nil { + arg1 = args[1].(ucan.Invocation) + } + var arg2 ucan.Container + if args[2] != nil { + arg2 = args[2].(ucan.Container) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockService_Publish_Call) Return(err error) *MockService_Publish_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockService_Publish_Call) RunAndReturn(run func(ctx context.Context, claim ucan.Invocation, meta ucan.Container) error) *MockService_Publish_Call { + _c.Call.Return(run) + return _c +} + +// Query provides a mock function for the type MockService +func (_mock *MockService) Query(ctx context.Context, q Query) (QueryResult, error) { + ret := _mock.Called(ctx, q) + + if len(ret) == 0 { + panic("no return value specified for Query") + } + + var r0 QueryResult + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, Query) (QueryResult, error)); ok { + return returnFunc(ctx, q) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, Query) QueryResult); ok { + r0 = returnFunc(ctx, q) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(QueryResult) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, Query) error); ok { + r1 = returnFunc(ctx, q) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockService_Query_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Query' +type MockService_Query_Call struct { + *mock.Call +} + +// Query is a helper method to define mock.On call +// - ctx context.Context +// - q Query +func (_e *MockService_Expecter) Query(ctx interface{}, q interface{}) *MockService_Query_Call { + return &MockService_Query_Call{Call: _e.mock.On("Query", ctx, q)} +} + +func (_c *MockService_Query_Call) Run(run func(ctx context.Context, q Query)) *MockService_Query_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 Query + if args[1] != nil { + arg1 = args[1].(Query) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockService_Query_Call) Return(queryResult QueryResult, err error) *MockService_Query_Call { + _c.Call.Return(queryResult, err) + return _c +} + +func (_c *MockService_Query_Call) RunAndReturn(run func(ctx context.Context, q Query) (QueryResult, error)) *MockService_Query_Call { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/types/mock_ValueSetCacheBatcher.go b/indexing-service/pkg/types/mock_ValueSetCacheBatcher.go new file mode 100644 index 00000000..1396b22b --- /dev/null +++ b/indexing-service/pkg/types/mock_ValueSetCacheBatcher.go @@ -0,0 +1,228 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package types + +import ( + "context" + + mock "github.com/stretchr/testify/mock" +) + +// NewMockValueSetCacheBatcher creates a new instance of MockValueSetCacheBatcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockValueSetCacheBatcher[Key any, Value any](t interface { + mock.TestingT + Cleanup(func()) +}) *MockValueSetCacheBatcher[Key, Value] { + mock := &MockValueSetCacheBatcher[Key, Value]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockValueSetCacheBatcher is an autogenerated mock type for the ValueSetCacheBatcher type +type MockValueSetCacheBatcher[Key any, Value any] struct { + mock.Mock +} + +type MockValueSetCacheBatcher_Expecter[Key any, Value any] struct { + mock *mock.Mock +} + +func (_m *MockValueSetCacheBatcher[Key, Value]) EXPECT() *MockValueSetCacheBatcher_Expecter[Key, Value] { + return &MockValueSetCacheBatcher_Expecter[Key, Value]{mock: &_m.Mock} +} + +// Add provides a mock function for the type MockValueSetCacheBatcher +func (_mock *MockValueSetCacheBatcher[Key, Value]) Add(ctx context.Context, key Key, values ...Value) error { + // Value + _va := make([]interface{}, len(values)) + for _i := range values { + _va[_i] = values[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, key) + _ca = append(_ca, _va...) + ret := _mock.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Add") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, Key, ...Value) error); ok { + r0 = returnFunc(ctx, key, values...) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockValueSetCacheBatcher_Add_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Add' +type MockValueSetCacheBatcher_Add_Call[Key any, Value any] struct { + *mock.Call +} + +// Add is a helper method to define mock.On call +// - ctx context.Context +// - key Key +// - values ...Value +func (_e *MockValueSetCacheBatcher_Expecter[Key, Value]) Add(ctx interface{}, key interface{}, values ...interface{}) *MockValueSetCacheBatcher_Add_Call[Key, Value] { + return &MockValueSetCacheBatcher_Add_Call[Key, Value]{Call: _e.mock.On("Add", + append([]interface{}{ctx, key}, values...)...)} +} + +func (_c *MockValueSetCacheBatcher_Add_Call[Key, Value]) Run(run func(ctx context.Context, key Key, values ...Value)) *MockValueSetCacheBatcher_Add_Call[Key, Value] { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 Key + if args[1] != nil { + arg1 = args[1].(Key) + } + var arg2 []Value + variadicArgs := make([]Value, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(Value) + } + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockValueSetCacheBatcher_Add_Call[Key, Value]) Return(err error) *MockValueSetCacheBatcher_Add_Call[Key, Value] { + _c.Call.Return(err) + return _c +} + +func (_c *MockValueSetCacheBatcher_Add_Call[Key, Value]) RunAndReturn(run func(ctx context.Context, key Key, values ...Value) error) *MockValueSetCacheBatcher_Add_Call[Key, Value] { + _c.Call.Return(run) + return _c +} + +// Commit provides a mock function for the type MockValueSetCacheBatcher +func (_mock *MockValueSetCacheBatcher[Key, Value]) Commit(ctx context.Context) error { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Commit") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = returnFunc(ctx) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockValueSetCacheBatcher_Commit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Commit' +type MockValueSetCacheBatcher_Commit_Call[Key any, Value any] struct { + *mock.Call +} + +// Commit is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockValueSetCacheBatcher_Expecter[Key, Value]) Commit(ctx interface{}) *MockValueSetCacheBatcher_Commit_Call[Key, Value] { + return &MockValueSetCacheBatcher_Commit_Call[Key, Value]{Call: _e.mock.On("Commit", ctx)} +} + +func (_c *MockValueSetCacheBatcher_Commit_Call[Key, Value]) Run(run func(ctx context.Context)) *MockValueSetCacheBatcher_Commit_Call[Key, Value] { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockValueSetCacheBatcher_Commit_Call[Key, Value]) Return(err error) *MockValueSetCacheBatcher_Commit_Call[Key, Value] { + _c.Call.Return(err) + return _c +} + +func (_c *MockValueSetCacheBatcher_Commit_Call[Key, Value]) RunAndReturn(run func(ctx context.Context) error) *MockValueSetCacheBatcher_Commit_Call[Key, Value] { + _c.Call.Return(run) + return _c +} + +// SetExpirable provides a mock function for the type MockValueSetCacheBatcher +func (_mock *MockValueSetCacheBatcher[Key, Value]) SetExpirable(ctx context.Context, key Key, expires bool) error { + ret := _mock.Called(ctx, key, expires) + + if len(ret) == 0 { + panic("no return value specified for SetExpirable") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, Key, bool) error); ok { + r0 = returnFunc(ctx, key, expires) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockValueSetCacheBatcher_SetExpirable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetExpirable' +type MockValueSetCacheBatcher_SetExpirable_Call[Key any, Value any] struct { + *mock.Call +} + +// SetExpirable is a helper method to define mock.On call +// - ctx context.Context +// - key Key +// - expires bool +func (_e *MockValueSetCacheBatcher_Expecter[Key, Value]) SetExpirable(ctx interface{}, key interface{}, expires interface{}) *MockValueSetCacheBatcher_SetExpirable_Call[Key, Value] { + return &MockValueSetCacheBatcher_SetExpirable_Call[Key, Value]{Call: _e.mock.On("SetExpirable", ctx, key, expires)} +} + +func (_c *MockValueSetCacheBatcher_SetExpirable_Call[Key, Value]) Run(run func(ctx context.Context, key Key, expires bool)) *MockValueSetCacheBatcher_SetExpirable_Call[Key, Value] { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 Key + if args[1] != nil { + arg1 = args[1].(Key) + } + var arg2 bool + if args[2] != nil { + arg2 = args[2].(bool) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockValueSetCacheBatcher_SetExpirable_Call[Key, Value]) Return(err error) *MockValueSetCacheBatcher_SetExpirable_Call[Key, Value] { + _c.Call.Return(err) + return _c +} + +func (_c *MockValueSetCacheBatcher_SetExpirable_Call[Key, Value]) RunAndReturn(run func(ctx context.Context, key Key, expires bool) error) *MockValueSetCacheBatcher_SetExpirable_Call[Key, Value] { + _c.Call.Return(run) + return _c +} diff --git a/indexing-service/pkg/types/types.go b/indexing-service/pkg/types/types.go new file mode 100644 index 00000000..2e09a0c7 --- /dev/null +++ b/indexing-service/pkg/types/types.go @@ -0,0 +1,238 @@ +package types + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/url" + + "github.com/fil-forge/forge/protocol/blobindex" + contentcaps "github.com/fil-forge/forge/protocol/commands/content" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" + "github.com/ipni/go-libipni/find/model" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multicodec" + mh "github.com/multiformats/go-multihash" + cbg "github.com/whyrusleeping/cbor-gen" +) + +// ContextID describes the data used to calculate a context id for IPNI +type ContextID struct { + Space *did.DID + Hash mh.Multihash +} + +// EncodedContextID is the encoded form of context id data that is actually stored +// in IPNI +type EncodedContextID []byte + +// ToEncoded canonically encodes ContextID data +func (c ContextID) ToEncoded() (EncodedContextID, error) { + if c.Space == nil { + return EncodedContextID(c.Hash), nil + } + mh, err := mh.Sum(bytes.Join([][]byte{[]byte(c.Space.String()), c.Hash}, nil), mh.SHA2_256, -1) + return EncodedContextID(mh), err +} + +// ErrKeyNotFound means the key did not exist in the cache +var ErrKeyNotFound = errors.New("key not found") + +// Store describes a generic storage interface +type Store[Key, Value any] interface { + // Put adds (or replaces) an item in the store. + Put(ctx context.Context, key Key, value Value) error + // Get retrieves an existing item from the store. If the item does not exist, + // it should return [ErrKeyNotFound]. + Get(ctx context.Context, key Key) (Value, error) +} + +// ErrWrongRootCount indicates a car file with multiple roots being unable to interpret +// as a query result +var ErrWrongRootCount = errors.New("query result should have exactly one root") + +// ErrNoRootBlock indicates a root that is specified but not found in a CAR file +var ErrNoRootBlock = errors.New("query root block not found in car") + +// Cache describes a generic cache interface +type Cache[Key, Value any] interface { + Set(ctx context.Context, key Key, value Value, expires bool) error + SetExpirable(ctx context.Context, key Key, expires bool) error + Get(ctx context.Context, key Key) (Value, error) +} + +// ValueSetCache describes a cache interface whose values are sets +type ValueSetCache[Key, Value any] interface { + Add(ctx context.Context, key Key, values ...Value) (uint64, error) + SetExpirable(ctx context.Context, key Key, expires bool) error + Members(ctx context.Context, key Key) ([]Value, error) +} + +// BatchingValueSetCache is a value-set cache that can batch updates. +// Note: a batch is not a transaction. +type BatchingValueSetCache[Key, Value any] interface { + ValueSetCache[Key, Value] + Batch() ValueSetCacheBatcher[Key, Value] +} + +type ValueSetCacheBatcher[Key, Value any] interface { + Add(ctx context.Context, key Key, values ...Value) error + SetExpirable(ctx context.Context, key Key, expires bool) error + Commit(ctx context.Context) error +} + +// ProviderStore caches queries to IPNI +type ProviderStore BatchingValueSetCache[mh.Multihash, model.ProviderResult] + +// NoProviderStore caches which queries for providers returned no results +type NoProviderStore ValueSetCache[mh.Multihash, multicodec.Code] + +// ContentClaimsStore stores published content claims +type ContentClaimsStore Store[cid.Cid, ucan.Invocation] + +// ContentClaimsCache caches fetched content claims +type ContentClaimsCache Cache[cid.Cid, ucan.Invocation] + +// ShardedDagIndexStore caches fetched sharded dag indexes +type ShardedDagIndexStore Cache[EncodedContextID, blobindex.ShardedDagIndex] + +// Match narrows parameters for locating providers/claims for a set of multihashes +type Match struct { + Subject []did.DID +} + +// QueryType allows defining which claims a query is targeting. QueryTypeStandard targets all claims, +// i.e. Location, Index and Equals +type QueryType int + +const ( + QueryTypeStandard QueryType = iota + QueryTypeLocation + QueryTypeIndexOrLocation + QueryTypeStandardCompressed +) + +func (qt QueryType) String() string { + switch qt { + case QueryTypeStandard: + return "standard" + case QueryTypeLocation: + return "location" + case QueryTypeIndexOrLocation: + return "index_or_location" + case QueryTypeStandardCompressed: + return "standard_compressed" + default: + return "invalid" + } +} + +func ParseQueryType(queryTypeStr string) (QueryType, error) { + switch queryTypeStr { + case QueryTypeStandard.String(): + return QueryTypeStandard, nil + case QueryTypeLocation.String(): + return QueryTypeLocation, nil + case QueryTypeIndexOrLocation.String(): + return QueryTypeIndexOrLocation, nil + case QueryTypeStandardCompressed.String(): + return QueryTypeStandardCompressed, nil + default: + return 0, fmt.Errorf("invalid query type: %s", queryTypeStr) + } +} + +// Query is a query for several multihashes +type Query struct { + Type QueryType + Hashes []mh.Multihash + Match Match + // Delegations allowing the indexer to retrieve bytes from the network. These + // are typically `/content/retrieve` delegations for each subject (space) + // in the [Match] parameter. + // + // Delegations are sent in the `X-UCAN-Container` HTTP header and MUST NOT + // exceed 4kb in size. + Delegations []ucan.Delegation +} + +type Block struct { + Link cid.Cid + Data []byte +} + +// QueryResult is an encodable result of a query +type QueryResult interface { + Root() Block + Blocks() []Block + // Claims is a list of links to the root block of claims that can be found in this message + Claims() []cid.Cid + // Indexes is a list of links to the CID hash of archived sharded dag indexes that can be found in this + // message + Indexes() []cid.Cid +} + +type Getter interface { + // Get retrieves a claim that has been published or cached by the + // indexing service. No external sources are consulted. + Get(ctx context.Context, claim cid.Cid) (ucan.Invocation, error) +} + +type Publisher interface { + // Cache caches a claim with the service temporarily. + Cache(ctx context.Context, provider peer.AddrInfo, claim ucan.Invocation, meta ucan.Container) error + // Publish writes a claim to permanent storage, adds it to an IPNI + // advertisement, annnounces it to IPNI nodes and caches it. + Publish(ctx context.Context, claim ucan.Invocation, meta ucan.Container) error +} + +type Querier interface { + // Query allows claims to be queried by their subject (content CID). It + // returns claims as well as any relevant indexes. + Query(ctx context.Context, q Query) (QueryResult, error) +} + +// Service is the core methods of the indexing service. +type Service interface { + Getter + Publisher + Querier +} + +// RetrievalRequest is all the details needed for retrieving data from the +// network. At minimum it contains the URL to retrieve a blob from. +// +// Legacy retrievals will not carry any retrieval authorization information i.e. +// the Auth field will be nil. +type RetrievalRequest struct { + // URL where the blob may be requested from. + URL url.URL + // Optional byte range to request. + Range *contentcaps.Range + // UCAN authorization parameters. + Auth RetrievalAuth +} + +// RetrievalAuth are the details for a UCAN authorized content retrieval. +// +// The provided proofs are expected to contain, for example, a +// `/content/retrieve` delegated capability allowing content to be retrieved +// using UCAN authorization. +type RetrievalAuth struct { + // The Indexing Service UCAN signing key. + Issuer ucan.Issuer + // Identity of the storage node to retrieve data from. + Audience did.DID + // Command for retrieval invocation. + Command ucan.Command + // Subject of the retrieval invocation. + Subject did.DID + // Arguments for the retrieval invocation. + Arguments cbg.CBORMarshaler + // Delegations from the client (e.g. `/content/retrieve`) or a storage + // node (e.g. `/blob/retrieve`) to the indexing service authorizing retrieval. + Proofs []ucan.Delegation +} diff --git a/indexing-service/tools/metadata/parse.go b/indexing-service/tools/metadata/parse.go new file mode 100644 index 00000000..b86b8c75 --- /dev/null +++ b/indexing-service/tools/metadata/parse.go @@ -0,0 +1,70 @@ +// Parse base64 encoded metadata and print it. +// +// This program takes the base64 `Metadata` value encoded in a response from +// IPNI. e.g. https://cid.contact/cid/bafybeighzj4zh5ptwqvdndazjxoq7t3abr5cat3iekx6xttdquj7waeihm +// +// Usage: go run ./tools/metadata/parse.go +// +// Examples: +// +// go run ./tools/metadata/parse.go gID4AaNhY9gqWCUAAXESIMW3kLyk7pHCD2de1EPfTaItNlGvQ7FoUIl2VYlit9DUYWUAYWnYKlgmAAGCBBIg7pPaVlvlZ4ROzdm676yzyA66LvU3RjucTsUxz6nCDsU= +// Type: 0x3e0000 (Index Claim) +// Value: &{Index:bagbaiera52j5uvs34vtyitwn3g5o7lftzahlulxvg5ddxhcoyuy47kocb3cq Expiration:0 Claim:bafyreigfw6ilzjhoshba6z262rb56tncfu3fdl2dwfufbclwkwewfn6q2q} +// +// go run ./tools/metadata/parse.go gBI= +// Type: 0x900 (Transport Bitswap) +// Value: &{} +// +// go run ./tools/metadata/parse.go oBIA +// Type: 0x920 (Transport IPFS Gateway HTTP) +// Value: &{} +package main + +import ( + "encoding/base64" + "fmt" + "os" + + "github.com/fil-forge/go-ipni-tools/pkg/metadata" + "github.com/multiformats/go-multicodec" +) + +var protos = map[multicodec.Code]string{ + // Storacha + metadata.LocationCommitmentID: "Location Commitment", + metadata.IndexClaimID: "Index Claim", + metadata.EqualsClaimID: "Equals Claim", + // General + multicodec.TransportBitswap: "Transport Bitswap", + multicodec.TransportIpfsGatewayHttp: "Transport IPFS Gateway HTTP", + multicodec.TransportGraphsyncFilecoinv1: "Transport Graphsync Filecoin v1", +} + +func main() { + in := os.Args[1] + if in == "" { + panic("missing base64 metadata argument") + } + + metaBytes, err := base64.StdEncoding.DecodeString(in) + if err != nil { + panic(fmt.Errorf("decoding base64: %w", err)) + } + + md := metadata.MetadataContext.New() + err = md.UnmarshalBinary(metaBytes) + if err != nil { + panic(fmt.Errorf("decoding metadata: %w", err)) + } + + for p, n := range protos { + data := md.Get(p) + if data != nil { + fmt.Printf("Type:\t0x%x (%s)\n", int(p), n) + fmt.Printf("Value:\t%+v\n", data) + return + } + } + + fmt.Println("Unknown metadata") +} diff --git a/indexing-service/tools/queryresult/genfixture.go b/indexing-service/tools/queryresult/genfixture.go new file mode 100644 index 00000000..2386eefa --- /dev/null +++ b/indexing-service/tools/queryresult/genfixture.go @@ -0,0 +1,154 @@ +// Generates an archive of a query result that contains 1 index and 3 claims. +// +// The claims consist of 2 location claims, (for the index and a CAR blob) and +// 1 index claim, asserting the CID of the CAR blob is indexed by the index. +// +// The filename is for the content root multihash (the root of the DAG in the +// CAR blob). +package main + +import ( + "bytes" + crand "crypto/rand" + "fmt" + "io" + "math/rand/v2" + "net/url" + "os" + + "github.com/fil-forge/automobile" + "github.com/fil-forge/forge/indexing-service/pkg/service/queryresult" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/bytemap" + ctypes "github.com/fil-forge/forge/protocol/commands" + cassert "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/ucantone/multikey" + "github.com/fil-forge/ucantone/multikey/ed25519" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multibase" + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" +) + +func main() { + storageID := multikey.KeyIssuer(must(ed25519.Generate())) + uploadServiceID := multikey.KeyIssuer(must(ed25519.Generate())) + indexingServiceID := multikey.KeyIssuer(must(ed25519.Generate())) + space := multikey.KeyIssuer(must(ed25519.Generate())) + spaceDID := space.DID() + + blockBytes := randomBytes(32) + blockDigest := must(multihash.Sum(blockBytes, multihash.SHA2_256, -1)) + blockLink := cid.NewCidV1(cid.Raw, blockDigest) + + carRoots := []cid.Cid{blockLink} + carBlock := automobile.Block{Link: blockLink, Data: blockBytes} + carBytes := must(io.ReadAll(automobile.Encode(carRoots, []automobile.Block{carBlock}))) + carDigest := must(multihash.Sum(carBytes, multihash.SHA2_256, -1)) + + _, ents, err := automobile.Index(bytes.NewReader(carBytes)) + if err != nil { + panic(err) + } + index := blobindex.NewShardedDagIndex(1) + for _, ent := range ents { + index.SetSlice(carDigest, ent.Link.Hash(), blobindex.Range{Start: int64(ent.Start), End: int64(ent.End)}) + } + var indexBuf bytes.Buffer + if err := blobindex.Archive(index, &indexBuf); err != nil { + panic(err) + } + indexBytes := indexBuf.Bytes() + indexDigest := must(multihash.Sum(indexBytes, multihash.SHA2_256, -1)) + indexLink := cid.NewCidV1(uint64(multicodec.Car), indexDigest) + + carLocationURL := blobURL(randomURL(), carDigest) + carLocationCommitment := must(cassert.Location.Invoke( + storageID, + storageID.DID(), + &cassert.LocationArguments{ + Space: space.DID(), + Content: carDigest, + Location: []ctypes.CborURL{carLocationURL}, + }, + invocation.WithAudience(space.DID()), + )) + + indexLocationURL := blobURL(randomURL(), indexDigest) + indexLocationCommitment := must(cassert.Location.Invoke( + storageID, + storageID.DID(), + &cassert.LocationArguments{ + Space: space.DID(), + Content: indexDigest, + Location: []ctypes.CborURL{indexLocationURL}, + }, + invocation.WithAudience(space.DID()), + )) + + indexClaim := must(cassert.Index.Invoke( + uploadServiceID, + uploadServiceID.DID(), + &cassert.IndexArguments{ + Index: indexLink, + }, + invocation.WithAudience(indexingServiceID.DID()), + )) + + claims := map[cid.Cid]ucan.Invocation{ + carLocationCommitment.Link(): carLocationCommitment, + indexLocationCommitment.Link(): indexLocationCommitment, + indexClaim.Link(): indexClaim, + } + + indexes := bytemap.NewByteMap[types.EncodedContextID, blobindex.ShardedDagIndex](1) + indexes.Set( + must(types.ContextID{Space: &spaceDID, Hash: indexDigest}.ToEncoded()), + index, + ) + + result := must(queryresult.Build(claims, indexes)) + + var resultBuf bytes.Buffer + writer := automobile.NewWriter(&resultBuf) + writer.WriteHeader([]cid.Cid{result.Root().Link}) + for _, blk := range result.Blocks() { + if err := writer.WriteBlock(automobile.Block(blk)); err != nil { + panic(err) + } + } + + // verify result can be read + must(queryresult.Extract(&resultBuf)) + + filename := fmt.Sprintf("%s.queryresult.car", must(multibase.Encode(multibase.Base58BTC, blockDigest))) + if err := os.WriteFile(filename, resultBuf.Bytes(), 0644); err != nil { + panic(err) + } +} + +func must[T any](val T, err error) T { + if err != nil { + panic(err) + } + return val +} + +func randomBytes(size int) []byte { + bytes := make([]byte, size) + must(crand.Read(bytes)) + return bytes +} + +func randomURL() *url.URL { + port := 3000 + rand.IntN(1000) + return must(url.Parse(fmt.Sprintf("http://127.0.0.1:%d", port))) +} + +func blobURL(base *url.URL, digest multihash.Multihash) ctypes.CborURL { + url := base.JoinPath("/blob/%s", must(multibase.Encode(multibase.Base58BTC, digest))) + return ctypes.CborURL(*url) +} diff --git a/indexing-service/version.json b/indexing-service/version.json new file mode 100644 index 00000000..965bbc8d --- /dev/null +++ b/indexing-service/version.json @@ -0,0 +1,3 @@ +{ + "version": "v1.13.4" +} diff --git a/ingot/CLAUDE.md b/ingot/CLAUDE.md index 711a3576..fc4a621e 100644 --- a/ingot/CLAUDE.md +++ b/ingot/CLAUDE.md @@ -51,7 +51,7 @@ ingot depends only on these — it must **never** import `fil-forge/sprue` or - **`ucantone`** — UCAN 1.0 primitives: `principal` (ed25519), `did`, `ucan/{delegation,invocation,receipt,command}`, `binding`, `client`, `execution`. -- **`libforge`** — Forge capability bindings + helpers: +- **`protocol`** (in-repo) — Forge capability bindings + helpers: `commands/{blob,content,http,assert,ucan,index,provider,access}`, `blobindex` (sharded-dag-index), `ucan` (ProofStore), `ucan/retrieval`, `didmailto`, `receipt`. - **`fil-forge/hilt/pkg/{client,rpc/service,s3perm,sigv4}`** — the auth/tenant @@ -71,7 +71,7 @@ ingot depends only on these — it must **never** import `fil-forge/sprue` or UCAN uses the **binding pattern**: a capability is `binding.Bind[*Args,*OK](command)` (e.g. `blob.Allocate`) with `.Invoke`/`.Delegate`; proof chains come from a -`libforge/ucan.ProofStore`. Materially different from go-ucanto — the UCAN code is +`ucanlib.ProofStore` (from `github.com/fil-forge/ucantone/ucanlib`). Materially different from go-ucanto — the UCAN code is a real port, not an import rewrite. ## Package map @@ -172,7 +172,7 @@ Internal: (empty) token store, and the goose migration `PreStartHook`. Empty option when `cfg.Enabled` is false. -A host provides `*zap.Logger`, `*pgxpool.Pool`, libforge's `identity.Identity` (the +A host provides `*zap.Logger`, `*pgxpool.Pool`, `internal/identity`'s `Identity` (the agent) and sets `Config.UploadServiceURL`/`UploadServiceDID` (sprue) + `AuthServiceURL`/`AuthServiceDID` (hilt). The non-fx escape hatch is `New(ctx, ServerConfig, ServerDeps)`. diff --git a/ingot/DESIGN_NOTES.md b/ingot/DESIGN_NOTES.md index 80b92faf..ad26e82c 100644 --- a/ingot/DESIGN_NOTES.md +++ b/ingot/DESIGN_NOTES.md @@ -18,7 +18,7 @@ in [`CLAUDE.md`](./CLAUDE.md). - **Library.** A host imports the fx `Module(cfg)` (or `ServerModule` plus the non-fx `New(ctx, ServerConfig, ServerDeps)`) and supplies a logger, a - Postgres pool, and the **agent** identity (libforge `identity.Identity`); config names + Postgres pool, and the **agent** identity (`internal/identity`'s `Identity`); config names the sprue endpoint (`upload_service_url`/`_did`) and the hilt endpoint (`auth_service_url`/`_did`, with the `auth_service_proofs` delegation chains hilt issues to this agent). @@ -147,7 +147,7 @@ HEAD never decrypts. See `s3frontend/decrypt.go`. ## Identity & auth -- **agent**: the libforge `identity.Identity` the host provides (daemon: the `identity.key_file` PEM +- **agent**: the `internal/identity`'s `Identity` the host provides (daemon: the `identity.key_file` PEM key, wrapped as the `identity.service_id` did:web when set), the issuer of every outbound invocation to sprue, hilt, and piri. The agent's DID document is served at `/.well-known/did.json` on the S3 listener; peers diff --git a/ingot/Makefile b/ingot/Makefile index 21657475..3b94eb5c 100644 --- a/ingot/Makefile +++ b/ingot/Makefile @@ -16,7 +16,7 @@ GO ?= go # direct exec of a bare 'docker'. DOCKER := $(shell command -v docker) -# Container image via the monorepo-shared docker/Dockerfile (context = this dir). +# Container image via the monorepo-shared docker/Dockerfile (context = the repo root). IMAGE_BUILD_ARGS := --build-arg SERVICE=ingot --build-arg MAIN_PKG=./cmd/ingot .PHONY: build test vet itest image image-dev gen gen-check clean help @@ -35,11 +35,11 @@ vet: ## image: build the prod container via the shared docker/Dockerfile image: - $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/ingot:local . + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/ingot:local .. ## image-dev: build the dev container (debug toolset + dlv) image-dev: - $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/ingot:local-dev . + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/ingot:local-dev .. ## itest: run ingot's system suite (boots the Forge stack in Docker; ~10 min) # The suite lives in smelt/tests/s3 — it validates the whole stack through diff --git a/ingot/README.md b/ingot/README.md index d4ada56b..708c4033 100644 --- a/ingot/README.md +++ b/ingot/README.md @@ -94,7 +94,7 @@ Postgres pool, and the agent identity (via `Config`): ```go app := fx.New( - // host provides: *zap.Logger, *pgxpool.Pool, libforge identity.Identity + // host provides: *zap.Logger, *pgxpool.Pool, `internal/identity`'s Identity ingot.Module(cfg), // cfg sets UploadServiceURL/DID, AuthServiceURL/DID, ... ) ``` @@ -155,7 +155,7 @@ them the publish step fails and the image still lands in GHCR. ## Dependencies ingot depends only on the Forge stack — `ucantone` (UCAN 1.0 primitives), -`libforge` (Forge capability definitions), the `hilt` client (request +`protocol` (Forge capability definitions), the `hilt` client (`internal/client/hilt`) (request authorization + tenancy), the `indexing-service` query client, `versitygw` (the S3 front end) — plus standard plumbing (pgx, goose, fx, zap, go-cid). It must never import `fil-forge/sprue` or `fil-forge/guppy` (guppy embeds ingot diff --git a/ingot/blockstore/forge.go b/ingot/blockstore/forge.go index 968c108a..718e6149 100644 --- a/ingot/blockstore/forge.go +++ b/ingot/blockstore/forge.go @@ -8,12 +8,12 @@ import ( "net/http" "net/url" - "github.com/fil-forge/libforge/blobindex" - "github.com/fil-forge/libforge/digestutil" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/digestutil" - indexclient "github.com/fil-forge/indexing-service/pkg/client" - contentcmds "github.com/fil-forge/libforge/commands/content" - "github.com/fil-forge/libforge/ucan/retrieval" + indexclient "github.com/fil-forge/forge/indexing-service/pkg/client" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" + "github.com/fil-forge/forge/protocol/retrieval" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ucan" diff --git a/ingot/blockstore/forge_test.go b/ingot/blockstore/forge_test.go index 2d1a2c9d..dd3197ee 100644 --- a/ingot/blockstore/forge_test.go +++ b/ingot/blockstore/forge_test.go @@ -6,13 +6,13 @@ import ( "strings" "testing" - "github.com/fil-forge/libforge/blobindex" - commands "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/testutil" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/protocol/blobindex" + commands "github.com/fil-forge/forge/protocol/commands" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey/ed25519" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/container" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" mh "github.com/multiformats/go-multihash" diff --git a/ingot/blockstore/locator/indexlocator.go b/ingot/blockstore/locator/indexlocator.go index c518f665..f390d286 100644 --- a/ingot/blockstore/locator/indexlocator.go +++ b/ingot/blockstore/locator/indexlocator.go @@ -8,11 +8,11 @@ import ( "strings" "sync" - indexclient "github.com/fil-forge/indexing-service/pkg/client" - "github.com/fil-forge/indexing-service/pkg/types" - "github.com/fil-forge/libforge/blobindex" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/digestutil" + indexclient "github.com/fil-forge/forge/indexing-service/pkg/client" + "github.com/fil-forge/forge/indexing-service/pkg/types" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/invocation" diff --git a/ingot/blockstore/locator/locator.go b/ingot/blockstore/locator/locator.go index dd14b7ae..11fda649 100644 --- a/ingot/blockstore/locator/locator.go +++ b/ingot/blockstore/locator/locator.go @@ -4,10 +4,10 @@ import ( "context" "fmt" - "github.com/fil-forge/libforge/blobindex" - "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/digestutil" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/ucantone/did" mh "github.com/multiformats/go-multihash" ) diff --git a/ingot/blockstore/log.go b/ingot/blockstore/log.go index 7b63a13a..caccf29f 100644 --- a/ingot/blockstore/log.go +++ b/ingot/blockstore/log.go @@ -24,7 +24,7 @@ type OpRoot struct { // notably the flush path that builds a ShardedDagIndexView) read // the entries to avoid rescanning the file. // TODO(forrest): replace Offset and Length with Start and End to -// align with libforge semantics +// align with the protocol module's semantics type BlockLoc struct { Offset uint64 Length uint64 diff --git a/ingot/bucketauthority/service.go b/ingot/bucketauthority/service.go index 149f7224..22b7fe93 100644 --- a/ingot/bucketauthority/service.go +++ b/ingot/bucketauthority/service.go @@ -5,10 +5,10 @@ import ( "errors" "fmt" - hiltclient "github.com/fil-forge/hilt/pkg/client" - bucketrpc "github.com/fil-forge/hilt/pkg/rpc/service/bucket" - "github.com/fil-forge/libforge/commands/s3" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" + hiltclient "github.com/fil-forge/forge/internal/client/hilt" + "github.com/fil-forge/forge/protocol/commands/s3" + bucketrpc "github.com/fil-forge/forge/protocol/commands/s3/bucket" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" "github.com/fil-forge/ucantone/did" ucanerr "github.com/fil-forge/ucantone/errors" ) diff --git a/ingot/cmd/deps.go b/ingot/cmd/deps.go index be27cdee..d5dc7675 100644 --- a/ingot/cmd/deps.go +++ b/ingot/cmd/deps.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/jackc/pgx/v5/pgxpool" "go.uber.org/zap" diff --git a/ingot/cmd/deps_test.go b/ingot/cmd/deps_test.go index 0d2fdb8e..9b55cb6e 100644 --- a/ingot/cmd/deps_test.go +++ b/ingot/cmd/deps_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/forge/ingot/config" diff --git a/ingot/cmd/serve_test.go b/ingot/cmd/serve_test.go index c5e2ad90..42df7c44 100644 --- a/ingot/cmd/serve_test.go +++ b/ingot/cmd/serve_test.go @@ -5,8 +5,8 @@ import ( "path/filepath" "testing" - blobcmds "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" diff --git a/ingot/config/config_test.go b/ingot/config/config_test.go index 683b5c7e..0a8d3a65 100644 --- a/ingot/config/config_test.go +++ b/ingot/config/config_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - blobcmds "github.com/fil-forge/libforge/commands/blob" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" diff --git a/ingot/config/util_test.go b/ingot/config/util_test.go index c3a3e653..bd57c7d4 100644 --- a/ingot/config/util_test.go +++ b/ingot/config/util_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - blobcmds "github.com/fil-forge/libforge/commands/blob" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" diff --git a/ingot/diddoc_test.go b/ingot/diddoc_test.go index 0c97a9e0..83715914 100644 --- a/ingot/diddoc_test.go +++ b/ingot/diddoc_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/did/web" "github.com/gofiber/fiber/v3" ) diff --git a/ingot/forgeclient/accessdelegate.go b/ingot/forgeclient/accessdelegate.go index 8a41c78f..05a77670 100644 --- a/ingot/forgeclient/accessdelegate.go +++ b/ingot/forgeclient/accessdelegate.go @@ -5,7 +5,7 @@ import ( "context" "fmt" - "github.com/fil-forge/libforge/commands/access" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ucan" diff --git a/ingot/forgeclient/accounts_test.go b/ingot/forgeclient/accounts_test.go index 48a515f9..ece8449a 100644 --- a/ingot/forgeclient/accounts_test.go +++ b/ingot/forgeclient/accounts_test.go @@ -5,8 +5,8 @@ import ( "net/url" "testing" + "github.com/fil-forge/forge/attestation/didmailto" "github.com/fil-forge/forge/ingot/tokenstore" - "github.com/fil-forge/libforge/attestation/didmailto" "github.com/fil-forge/ucantone/absentee" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey/ed25519" diff --git a/ingot/forgeclient/blobabort.go b/ingot/forgeclient/blobabort.go index afc338e1..41db854f 100644 --- a/ingot/forgeclient/blobabort.go +++ b/ingot/forgeclient/blobabort.go @@ -4,11 +4,11 @@ import ( "context" "fmt" - blobcmds "github.com/fil-forge/libforge/commands/blob" - ucanlib "github.com/fil-forge/libforge/ucan" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ucan/invocation" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" "github.com/multiformats/go-multihash" ) diff --git a/ingot/forgeclient/blobadd.go b/ingot/forgeclient/blobadd.go index d27c76da..df37a68a 100644 --- a/ingot/forgeclient/blobadd.go +++ b/ingot/forgeclient/blobadd.go @@ -25,12 +25,11 @@ import ( "slices" "time" - assertcmds "github.com/fil-forge/libforge/commands/assert" - blobcmds "github.com/fil-forge/libforge/commands/blob" - httpcmds "github.com/fil-forge/libforge/commands/http" - ucancmds "github.com/fil-forge/libforge/commands/ucan" - receipt_client "github.com/fil-forge/libforge/receipt" - ucanlib "github.com/fil-forge/libforge/ucan" + assertcmds "github.com/fil-forge/forge/protocol/commands/assert" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + httpcmds "github.com/fil-forge/forge/protocol/commands/http" + ucancmds "github.com/fil-forge/forge/protocol/commands/ucan" + receipt_client "github.com/fil-forge/forge/protocol/receipt" "github.com/fil-forge/ucantone/did" edm "github.com/fil-forge/ucantone/errors/datamodel" "github.com/fil-forge/ucantone/execution" @@ -41,6 +40,7 @@ import ( "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/fil-forge/ucantone/ucan/receipt" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" "github.com/multiformats/go-multihash" "go.uber.org/zap" diff --git a/ingot/forgeclient/blobremove.go b/ingot/forgeclient/blobremove.go index a2a05edd..82d08890 100644 --- a/ingot/forgeclient/blobremove.go +++ b/ingot/forgeclient/blobremove.go @@ -4,11 +4,11 @@ import ( "context" "fmt" - blobcmds "github.com/fil-forge/libforge/commands/blob" - ucanlib "github.com/fil-forge/libforge/ucan" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ucan/invocation" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/multiformats/go-multihash" ) diff --git a/ingot/forgeclient/claimaccess.go b/ingot/forgeclient/claimaccess.go index f87022b4..1a7f2c58 100644 --- a/ingot/forgeclient/claimaccess.go +++ b/ingot/forgeclient/claimaccess.go @@ -6,8 +6,8 @@ import ( "fmt" "slices" - accesscmds "github.com/fil-forge/libforge/commands/access" - attestcmds "github.com/fil-forge/libforge/commands/ucan/attest" + attestcmds "github.com/fil-forge/forge/attestation/attest" + accesscmds "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ucan" diff --git a/ingot/forgeclient/client.go b/ingot/forgeclient/client.go index c99ebb8c..8992f775 100644 --- a/ingot/forgeclient/client.go +++ b/ingot/forgeclient/client.go @@ -18,7 +18,7 @@ import ( "github.com/fil-forge/forge/ingot/internal/ucanexec" "github.com/fil-forge/forge/ingot/tokenstore" - "github.com/fil-forge/libforge/receipt" + "github.com/fil-forge/forge/protocol/receipt" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" diff --git a/ingot/forgeclient/indexadd.go b/ingot/forgeclient/indexadd.go index 4de6965f..b84537a9 100644 --- a/ingot/forgeclient/indexadd.go +++ b/ingot/forgeclient/indexadd.go @@ -5,15 +5,15 @@ import ( "context" "fmt" - contentcmds "github.com/fil-forge/libforge/commands/content" - indexcmds "github.com/fil-forge/libforge/commands/index" - ucanlib "github.com/fil-forge/libforge/ucan" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" + indexcmds "github.com/fil-forge/forge/protocol/commands/index" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ipld/datamodel" "github.com/fil-forge/ucantone/ucan/delegation" "github.com/fil-forge/ucantone/ucan/delegation/policy" "github.com/fil-forge/ucantone/ucan/invocation" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" ) diff --git a/ingot/forgeclient/options.go b/ingot/forgeclient/options.go index f8f9839c..74c28b77 100644 --- a/ingot/forgeclient/options.go +++ b/ingot/forgeclient/options.go @@ -3,7 +3,7 @@ package forgeclient import ( "github.com/fil-forge/forge/ingot/tokenstore" - "github.com/fil-forge/libforge/receipt" + "github.com/fil-forge/forge/protocol/receipt" "github.com/fil-forge/ucantone/client" "go.uber.org/zap" ) diff --git a/ingot/forgeclient/pollclaim.go b/ingot/forgeclient/pollclaim.go index a1cb28e2..fe5ad58a 100644 --- a/ingot/forgeclient/pollclaim.go +++ b/ingot/forgeclient/pollclaim.go @@ -8,7 +8,7 @@ import ( "fmt" "time" - "github.com/fil-forge/libforge/commands/access" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/ucantone/ipld/datamodel" "github.com/fil-forge/ucantone/result" "github.com/fil-forge/ucantone/ucan" diff --git a/ingot/forgeclient/proofstore_test.go b/ingot/forgeclient/proofstore_test.go index 10a27f2b..9e778684 100644 --- a/ingot/forgeclient/proofstore_test.go +++ b/ingot/forgeclient/proofstore_test.go @@ -8,9 +8,9 @@ import ( "testing" "github.com/fil-forge/forge/ingot/tokenstore" - blobcmds "github.com/fil-forge/libforge/commands/blob" - contentcmds "github.com/fil-forge/libforge/commands/content" - indexcmds "github.com/fil-forge/libforge/commands/index" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" + indexcmds "github.com/fil-forge/forge/protocol/commands/index" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" diff --git a/ingot/forgeclient/provideradd.go b/ingot/forgeclient/provideradd.go index 40e9d28a..ea0674a4 100644 --- a/ingot/forgeclient/provideradd.go +++ b/ingot/forgeclient/provideradd.go @@ -5,7 +5,7 @@ import ( "context" "fmt" - providercmds "github.com/fil-forge/libforge/commands/provider" + providercmds "github.com/fil-forge/forge/protocol/commands/provider" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ucan/invocation" diff --git a/ingot/forgeclient/requestaccess.go b/ingot/forgeclient/requestaccess.go index 97bf8248..52874ab7 100644 --- a/ingot/forgeclient/requestaccess.go +++ b/ingot/forgeclient/requestaccess.go @@ -5,7 +5,7 @@ import ( "context" "fmt" - "github.com/fil-forge/libforge/commands/access" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan/command" "github.com/fil-forge/ucantone/ucan/invocation" diff --git a/ingot/go.mod b/ingot/go.mod index 113c5dc6..d48fd17a 100644 --- a/ingot/go.mod +++ b/ingot/go.mod @@ -4,14 +4,15 @@ go 1.27.0 require ( github.com/alanshaw/dag-json-gen v0.0.9 - github.com/aws/aws-sdk-go-v2 v1.43.8 - github.com/aws/aws-sdk-go-v2/service/s3 v1.107.4 + github.com/aws/aws-sdk-go-v2 v1.45.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.110.0 github.com/cenkalti/backoff/v5 v5.0.3 - github.com/fil-forge/hilt v0.0.1-0.20260828114936-cb1bc0b84e7b - github.com/fil-forge/indexing-service v1.13.5-0.20260820151722-9eb620409a4e - github.com/fil-forge/libforge v0.0.0-20260827180828-c9252ac89b0e + github.com/fil-forge/forge/attestation v0.0.0 + github.com/fil-forge/forge/indexing-service v0.0.0 + github.com/fil-forge/forge/internal v0.0.0 + github.com/fil-forge/forge/protocol v0.0.0 github.com/fil-forge/swarf v0.0.1-0.20260821142121-d5d1a0a56f00 - github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a github.com/filecoin-project/go-fee v0.1.1-0.20260826122013-1c3970b90b0d github.com/gofiber/fiber/v3 v3.5.0 @@ -45,27 +46,28 @@ require ( github.com/DataDog/datadog-go/v5 v5.9.0 // indirect github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect github.com/andybalholm/brotli v1.2.2 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.19 // indirect - github.com/aws/aws-sdk-go-v2/config v1.32.39 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.38 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.39 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect + github.com/aws/aws-sdk-go-v2/config v1.33.2 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.20.2 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.12 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.39 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.39 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.40 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.32 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.39 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.40 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.5.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.33.8 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.45.8 // indirect - github.com/aws/smithy-go v1.27.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.1 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.36.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.41.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.48.0 // indirect + github.com/aws/smithy-go v1.28.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/fil-forge/automobile v0.0.1 // indirect + github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect @@ -132,12 +134,10 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect - gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect - gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.45.0 // indirect - go.opentelemetry.io/otel/metric v1.45.0 // indirect - go.opentelemetry.io/otel/trace v1.45.0 // indirect + go.opentelemetry.io/otel v1.46.0 // indirect + go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/otel/trace v1.46.0 // indirect go.uber.org/dig v1.19.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.55.0 // indirect @@ -151,3 +151,11 @@ require ( lukechampine.com/blake3 v1.4.1 // indirect pitr.ca/jsontokenizer v0.3.2 // indirect ) + +replace github.com/fil-forge/forge/protocol => ../protocol + +replace github.com/fil-forge/forge/internal => ../internal + +replace github.com/fil-forge/forge/indexing-service => ../indexing-service + +replace github.com/fil-forge/forge/attestation => ../attestation diff --git a/ingot/go.sum b/ingot/go.sum index 9d66a21d..1e064b1b 100644 --- a/ingot/go.sum +++ b/ingot/go.sum @@ -1,7 +1,3 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw= github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/DataDog/datadog-go/v5 v5.9.0 h1:0rhs5wBov9Iz+xLXLk4maaReHvOANM1ijSm2IKWtKFs= @@ -15,60 +11,48 @@ github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktp github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/aws/aws-sdk-go-v2 v1.43.8 h1:fpnrxwuwsoGIgjvgLeDU3y9w7YaHBxyF6AF3vQL8duw= -github.com/aws/aws-sdk-go-v2 v1.43.8/go.mod h1:j7gYSq8dL95QejkFXxvQNESH4I9WGHFI6iO+vhqEi5Q= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.19 h1:56S0VBF43Kvy2YiWkZe65Uj5rpvW1LLnHBUBg8jlxuQ= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.19/go.mod h1:n1TtGWnXCj/kl11tMcNN2aeS95u01phKYgE39N5HHfM= -github.com/aws/aws-sdk-go-v2/config v1.32.39 h1:3TYUWYWawsE9KF02G3dA7vsbwoCphyGOpFFEUugRs/4= -github.com/aws/aws-sdk-go-v2/config v1.32.39/go.mod h1:/lPP/ciQurgJa6l6mbBX+b5MB1qaLrC9dd3YHtGvrhk= -github.com/aws/aws-sdk-go-v2/credentials v1.19.38 h1:Xf8j1+vzwPRCta9pFXjj0677BzXrRO2JbpAVNcdXnnI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.38/go.mod h1:PGYzFTznwRAJ2q0m+oX+P8SlfZQKpBAKQCokNuMl3Sg= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.39 h1:9GLrXl8PKQ3+bMniXFg3vliMWJ+204bFcIvBCwJFglc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.39/go.mod h1:MmlE5TLgq7+QbXKKUSzqUz4h0Uu5kz2SEe6iPX+ZFHI= +github.com/aws/aws-sdk-go-v2 v1.45.1 h1:iIoG3NaLhV6UZpPXyPXlDj2I9oS8tV/nMcMnITCC6Ks= +github.com/aws/aws-sdk-go-v2 v1.45.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8= +github.com/aws/aws-sdk-go-v2/config v1.33.2 h1:Pj4+nF2kc4Z+1BJysVPnX9d5dMN7IYFXR4UJaWK2IpA= +github.com/aws/aws-sdk-go-v2/config v1.33.2/go.mod h1:Igw+HTwbR2tsTU/ydifAS9EHAFJ2s/FCgkwQWFnAdE4= +github.com/aws/aws-sdk-go-v2/credentials v1.20.2 h1:VQjZODPNfdikCX2ZZrltw4zNLkcwjyUFDUl2vT9yTwg= +github.com/aws/aws-sdk-go-v2/credentials v1.20.2/go.mod h1:OmeHCn28vZylsBvalLDf7t8fuJ2rHYQprJs+7WuxniI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 h1:YIEBqcqRnpi4Pfv0YHImtgi6czGCwKHANC7SwmUAVD0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1/go.mod h1:imEf0oufgAo8KAkCHhrOdqGEC0YWx1PPBQH82shSxGw= github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.12 h1:0jSSxdkgDH6ShgraXXk//B/UGi73aRgoE5PTVCM26wM= github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.12/go.mod h1:rdg/WbW8wInpiUA/6qzLDwn97IPD8d3pvfMbCBWslMA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.39 h1:YrEI22hVQcqMpq934ZoPQyJjGNzX4CGdrSDCjBD59sI= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.39/go.mod h1:N8qOX83LkaCeizvrfiNjwkBOXkxHt6a74CiZn8qz9F8= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.39 h1:Vo7UZzBjB6zS6feEOuBlpEgaj8iBTdiNlye+7w9ooGo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.39/go.mod h1:JgxtAO/77e95Rs9WMWUzz99hT182gqdAh7/DHuEMA/k= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.40 h1:oofDq8Y5M82fmDrxb8gsbP0LS73MqZ388qKVgs5ETYI= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.40/go.mod h1:LSfLmbvx50+T+/DoUZRqB1qS38v7lvNUebqIpidAWYM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.18 h1:+fiwOxNdE8bOK3SoVTln8hwP+OCyArbi2/InIr/A9AU= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.18/go.mod h1:aua4m7EZSvQra/96b8zJxWHwtHxuXQ8bx4DiM92V044= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.32 h1:GDKKLoFujnrZkWJAbfgDvX2cb0TP73JeQQc9fVK4BfE= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.32/go.mod h1:wQJ9fj9RPoeHImfpG4NwPInNpwamTI539nK8bFMX+ew= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.39 h1:inoUrqz4Lfpw1XwpUvQnBiAJ2tUzn3opZ0gduNLxo+8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.39/go.mod h1:Yx+RrmAF+XGZTccwhQ3o4K5V8qkZBsTAcq148Y8g57k= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.40 h1:omoUTxUzc1jb9yMa+7Y86R+/8MzsdjrR/juI60b4RLc= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.40/go.mod h1:ZP7gNAEnLFigr5CEX5tdU7xWbj52noH2m8IAeIhFgCY= -github.com/aws/aws-sdk-go-v2/service/s3 v1.107.4 h1:R1hIw5Z7OqINqgYo5LykHQo2BWbcflFzGaonJgNkbao= -github.com/aws/aws-sdk-go-v2/service/s3 v1.107.4/go.mod h1:oinlf/VTl4hAUctSvIaOPKOZbckTIaWzYj96MRbPKb4= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.8 h1:bghrxelVQpGurGI1X94BT68h6p+hWQnlsu8nSmiSll4= -github.com/aws/aws-sdk-go-v2/service/signin v1.5.8/go.mod h1:gkwdIl9w+6LFKlGRLz3+Dw+cudc9dD1ViMDhHGmzOgk= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.8 h1:/DbiPZ8maO03uFnXa6yEhFdWOTA5xObmGNfaEzt9Cac= -github.com/aws/aws-sdk-go-v2/service/sso v1.33.8/go.mod h1:mUywXl2WlN+gZD0vNeg1Hn0EMOifDQ79StJcdqXHkXo= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.8 h1:wv4pCyq/LkBYc5R4m/g5S+uGqF/DbL+bp9VXiQEnec4= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.8/go.mod h1:9AKVT0vADSCPXRuoZjziHwsbdLDFMGRExwWBQourCa8= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.8 h1:oQrmuqpBAExYPEPJp8dkj9KLmc0y42iwvAV28OwlzF0= -github.com/aws/aws-sdk-go-v2/service/sts v1.45.8/go.mod h1:qNTXKrmzx2cC6VmM7PxHNasBMWKx3mfxgzcbVjcWVAU= -github.com/aws/smithy-go v1.27.10 h1:bw56MIx8bhTQZSdzucEJSKWLpwX0ju7hU8cVoa75dg8= -github.com/aws/smithy-go v1.27.10/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 h1:pc138gM1CW+XPc60rEwUlwwuwWFQK16CI1T7v1F9Oec= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1/go.mod h1:1+koxpPIbfBdfzP6vojm5/zTpTQ/micYwlxIiNB3TxI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 h1:K0JsbZQj+1h208Ro1zHeA4l7bMp0NvRffHQ91q8Ol1s= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1/go.mod h1:W3/vL6EtCIatICGy9ab29QhMuae+cOKPWcMxv02CO+Q= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 h1:yhw5KD1phVyP9vijxOUzDfEtJx+bt+L63k+VfuiYFAA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1/go.mod h1:ZW2e0d7DYlRxlS9hEiMXE47gTdX5KRN4byUiNbUpG+Q= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.1 h1:s67hBfG5t9rn1NCvDuB4E3QIep3UFhHPtaIqFDjV3N8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.1/go.mod h1:FpvjBMXtSNMLPmDJsWwcY5cRnqJlpS2y1R6n4pvzs4k= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1 h1:RmmWQPREQdk9U+PfqeHW3MqZaBaNK7TpV9W3RY+b+7g= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.1/go.mod h1:0A3W4F+68ZnNk5XcNL/e9HFMwnP8RlEicFfy6eOEDyw= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.1 h1:ZMbtPZZQRca+3+XYQne9PBvRiYpHZlNJJOZfE9WNfT0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.1/go.mod h1:YAGWQdCYlVCoqrzvfv3RLxO6zKwti7gsAULOGWPLYv4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.110.0 h1:He8vaTTqAAJrux/KdpjFXNWueLJZyKqE49QEXoqAu4I= +github.com/aws/aws-sdk-go-v2/service/s3 v1.110.0/go.mod h1:CUr46sCpGAg/rHaclRyhJX0LJAmH73uWSJPPSaMUrSk= +github.com/aws/aws-sdk-go-v2/service/signin v1.8.0 h1:bSvKIoLuRGFqGwASgeCQncCJDi9YKKBDEmCEZzOX1uU= +github.com/aws/aws-sdk-go-v2/service/signin v1.8.0/go.mod h1:9IqUlsJDbUPcg6cgx3WEzXdjrbWzLDQrak0aaSqlTcI= +github.com/aws/aws-sdk-go-v2/service/sso v1.36.0 h1:iivsh357VnfIc18IFWSuoyQEluf8frfWf4cL2Y0JUQw= +github.com/aws/aws-sdk-go-v2/service/sso v1.36.0/go.mod h1:tWuiVBUtPBr8/rgRiYS8Uf85sHcAN+G7XS3D3CEoUh8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.41.0 h1:wVxM3QzSKIK8tSN6OGgezp9OK91lCLH2zhmRInN9rFM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.41.0/go.mod h1:naFe83jSMuYkH+QjQPX8n1MLhBkeCFM5Lsnh5m5wz3c= +github.com/aws/aws-sdk-go-v2/service/sts v1.48.0 h1:RzZVCzYM19vhJCT5s6vO2wN8ie770Li/TmbAZ9B6N7E= +github.com/aws/aws-sdk-go-v2/service/sts v1.48.0/go.mod h1:mKo/CzaCz8qytGW70NG4vIIGAx1HXTlb5lHNkC5k3lk= +github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ= +github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4= -github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= -github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= -github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -78,36 +62,20 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= -github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY= -github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= -github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/fil-forge/automobile v0.0.1 h1:9xB3yc4l5b9EdRJSJcNwudgBFNHoMPEAdcb7GfobLhA= github.com/fil-forge/automobile v0.0.1/go.mod h1:TsO7jlO8ykJZY5tF8j4GsUcu3F02lEzxO7ULoB61hRA= github.com/fil-forge/go-ipni-tools v0.0.0-20260820150418-78ca0c16834d h1:iEXJC5aJgRHq2iM4X6usfow436OQGauLjDuga1Hiq4o= github.com/fil-forge/go-ipni-tools v0.0.0-20260820150418-78ca0c16834d/go.mod h1:fwLjqz34DvfoWcuzyH8Wr7dTJdr3mzYjxZRweysHOH4= -github.com/fil-forge/hilt v0.0.1-0.20260828114936-cb1bc0b84e7b h1:6T/ziPaWKHc5evCFgP4boWCvqe6OvNeQksuQXYBNGDI= -github.com/fil-forge/hilt v0.0.1-0.20260828114936-cb1bc0b84e7b/go.mod h1:Vj4E86/Bp0yC36XrZd0gBGYQEF9GA9o1SX99kVHVtnI= -github.com/fil-forge/indexing-service v1.13.5-0.20260820151722-9eb620409a4e h1:+A26/La8EgF1bsGhSGicYV97ckec1SJwNYLWee3Xo8g= -github.com/fil-forge/indexing-service v1.13.5-0.20260820151722-9eb620409a4e/go.mod h1:hrry50c9bNciNj9Z63O2QFrzidnTjkYYG2UgH4/rmVo= -github.com/fil-forge/libforge v0.0.0-20260827180828-c9252ac89b0e h1:2na0I6mpUhgwW6tg67S9rTFVOkr9gchdZFvG44QcA0I= -github.com/fil-forge/libforge v0.0.0-20260827180828-c9252ac89b0e/go.mod h1:09nIz6/BJD0Qdb8YrLwVeNQwQprTCFWVPULL3c5CNJs= +github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5 h1:Pu7Xk/LdaQyv1560k1MbL6CQUdWxZJIG9elEE0vkgLY= +github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5/go.mod h1:aL/KcXZ0KVs1ymAYqIwQyCnl2p481InN+gYbRLNM8fI= github.com/fil-forge/swarf v0.0.1-0.20260821142121-d5d1a0a56f00 h1:uSJ2YCxOJQA7PDnDiBJAOstoJcw2IPnbpssKYCihsxg= github.com/fil-forge/swarf v0.0.1-0.20260821142121-d5d1a0a56f00/go.mod h1:k1gay5Byjloml4k1SylxhRff1rMH4LcGKpbMEDLZR1o= -github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1 h1:Tzy3lZ7+LAyVK4+qyWM97UMOtnXx05Wdn+rcTDLdBDE= -github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a h1:lDwnNmF4LNbevx/YYNCC0CazMiL4eIe38MvfqbRR3RA= github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a/go.mod h1:t73Wa2xqpT0NdY+SzRZDiO+tQD13b3UEZRoe2wYFiIE= github.com/filecoin-project/go-fee v0.1.1-0.20260826122013-1c3970b90b0d h1:W+UJ3I79FKfdY0EcNelK1APMYXBZgXfx2OPLOiVEb7E= @@ -129,8 +97,6 @@ github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/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.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -197,8 +163,6 @@ github.com/ipld/go-ipld-prime v0.24.0 h1:6th8Z6Peh5bCWuRAVZcDO1sHzZdVF6F2cCCDG36 github.com/ipld/go-ipld-prime v0.24.0/go.mod h1:DYZxr/5caLNFbcuU6zLOgwSW7CgUEoC4wJiZMEU8Zhs= github.com/ipni/go-libipni v0.8.2 h1:mRJe602pD3WKcl/2WJFT6CjdI181/DcThqEdRL0usA8= github.com/ipni/go-libipni v0.8.2/go.mod h1:YlujVRDn3uksrYeV62ERtqKQdRVgbnjF13wPsnCCHAk= -github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ= -github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -233,10 +197,6 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-libp2p v0.49.0 h1:ibXuYPIHmMIPShob1BktQvSuFQkq/MemhQOLKfGujjw= github.com/libp2p/go-libp2p v0.49.0/go.mod h1:lzjVcOBk5fCn1QD2XbSOKLZesB6gEsry8SLjCsAAGT4= -github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= -github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= @@ -251,24 +211,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= -github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= -github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= -github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= -github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= -github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= -github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= -github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= -github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= -github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= @@ -297,10 +239,6 @@ github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= github.com/oklog/ulid/v2 v2.1.2/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/openbao/openbao/api/v2 v2.6.0 h1:KvfspAaL9bab9hI8jFYkV2cgtSrwWtaG+k9AUTHWU4M= github.com/openbao/openbao/api/v2 v2.6.0/go.mod h1:H4IWiH+2rgF/TbrsUbsfrMyGoqojkLqxPCRLENSMnSo= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= @@ -310,13 +248,10 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.90.0 h1:58BfEsP+G4uIRD9ApJTFsag+Mw+QQlZuH9uI/lPmjfY= github.com/polydawn/refmt v0.90.0/go.mod h1:XAlDMOunevTYDsZtOKQd8itHXFMsX/QtDkPHaj6ZLxk= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= -github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pressly/goose/v3 v3.27.2 h1:FjKNzcmMdGrQlSIu5alMSmakQtJFBgtw+A0bb1p/LC8= github.com/pressly/goose/v3 v3.27.2/go.mod h1:qWW+/8dkVtJYjJrbIpwD5xxnEJTUKvxkQ9JKQp9LaIM= github.com/rabbitmq/amqp091-go v1.12.0 h1:V0v14Iqfs+MwHWihJt/nGS5Ulu0vw572b2Co3mwunkI= @@ -336,11 +271,7 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shamaton/msgpack/v3 v3.2.0 h1:1q2Ms+MWmuRju+PuDMSFDB7p7621npeX4zprJN5Zck8= github.com/shamaton/msgpack/v3 v3.2.0/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo= -github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= -github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= @@ -377,16 +308,8 @@ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWD github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI= -github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE= -github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0 h1:GCbb1ndrF7OTDiIvxXyItaDab4qkzTFJ48LKFdM7EIo= -github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0/go.mod h1:IRPBaI8jXdrNfD0e4Zm7Fbcgaz5shKxOQv4axiL09xs= github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= -github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= -github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI= -github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4= -github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s= @@ -404,8 +327,6 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= @@ -416,14 +337,12 @@ gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAF gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= -go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= -go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= -go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= -go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= -go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= -go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= diff --git a/ingot/iam/keycache.go b/ingot/iam/keycache.go index b8f6fac5..12a20305 100644 --- a/ingot/iam/keycache.go +++ b/ingot/iam/keycache.go @@ -4,7 +4,7 @@ import ( "path" "time" - s3 "github.com/fil-forge/libforge/commands/s3" + s3 "github.com/fil-forge/forge/protocol/commands/s3" gocache "github.com/patrickmn/go-cache" ) diff --git a/ingot/iam/keycache_test.go b/ingot/iam/keycache_test.go index 9cd5f0c7..a9a57d1e 100644 --- a/ingot/iam/keycache_test.go +++ b/ingot/iam/keycache_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - s3 "github.com/fil-forge/libforge/commands/s3" + s3 "github.com/fil-forge/forge/protocol/commands/s3" "github.com/stretchr/testify/require" "github.com/fil-forge/forge/ingot/iam" diff --git a/ingot/iam/keyproofs_test.go b/ingot/iam/keyproofs_test.go index ebddddca..c0ac1941 100644 --- a/ingot/iam/keyproofs_test.go +++ b/ingot/iam/keyproofs_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - contentcmds "github.com/fil-forge/libforge/commands/content" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/delegation" diff --git a/ingot/iam/proofcache.go b/ingot/iam/proofcache.go index 95c15552..b887d981 100644 --- a/ingot/iam/proofcache.go +++ b/ingot/iam/proofcache.go @@ -6,9 +6,9 @@ import ( "sync" "time" - ucanlib "github.com/fil-forge/libforge/ucan" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" gocache "github.com/patrickmn/go-cache" ) diff --git a/ingot/iam/proofcache_internal_test.go b/ingot/iam/proofcache_internal_test.go index d72d6511..0ff6e712 100644 --- a/ingot/iam/proofcache_internal_test.go +++ b/ingot/iam/proofcache_internal_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - contentcmds "github.com/fil-forge/libforge/commands/content" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/delegation" diff --git a/ingot/iam/proofcache_test.go b/ingot/iam/proofcache_test.go index 02139caf..aa3ecc2e 100644 --- a/ingot/iam/proofcache_test.go +++ b/ingot/iam/proofcache_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - contentcmds "github.com/fil-forge/libforge/commands/content" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/delegation" diff --git a/ingot/iam/revoker_test.go b/ingot/iam/revoker_test.go index bd7458c0..6f59d313 100644 --- a/ingot/iam/revoker_test.go +++ b/ingot/iam/revoker_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - contentcmds "github.com/fil-forge/libforge/commands/content" - s3 "github.com/fil-forge/libforge/commands/s3" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" + s3 "github.com/fil-forge/forge/protocol/commands/s3" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" diff --git a/ingot/iam/service.go b/ingot/iam/service.go index dacc14b0..9c46608e 100644 --- a/ingot/iam/service.go +++ b/ingot/iam/service.go @@ -44,26 +44,26 @@ import ( "time" - hiltauth "github.com/fil-forge/hilt/pkg/rpc/service/auth" - "github.com/fil-forge/hilt/pkg/s3perm" - "github.com/fil-forge/hilt/pkg/sigv4" "github.com/fil-forge/forge/ingot/internal/fasthttputil" "github.com/fil-forge/forge/ingot/internal/reqscope" "github.com/fil-forge/forge/ingot/registry" - s3 "github.com/fil-forge/libforge/commands/s3" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" - s3req "github.com/fil-forge/libforge/commands/s3/request" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/internal/s3perm" + "github.com/fil-forge/forge/internal/sigv4" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + hiltauth "github.com/fil-forge/forge/protocol/commands/s3/request" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "github.com/fil-forge/ucantone/did" ucanerrors "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/ucan" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/fil-forge/versitygw/auth" "github.com/fil-forge/versitygw/s3api/middlewares" "github.com/fil-forge/versitygw/s3err" "github.com/gofiber/fiber/v3" "go.uber.org/zap" - hiltclient "github.com/fil-forge/hilt/pkg/client" + hiltclient "github.com/fil-forge/forge/internal/client/hilt" ) // Authorizer is the slice of [hiltclient.Client] the service uses: @@ -324,7 +324,7 @@ func (s *Service) authorizeLocal(ctx context.Context, req s3.Request, access str // 3. The S3 action must map to Forge commands. Bucket-level operations // (create/delete/list-buckets) map to none — they go to Hilt regardless. - op, err := hiltauth.OperationFor(req) + op, err := s3.OperationFor(req) if err != nil { return auth.Account{}, false } diff --git a/ingot/iam/service_internal_test.go b/ingot/iam/service_internal_test.go index 3103e436..245a21db 100644 --- a/ingot/iam/service_internal_test.go +++ b/ingot/iam/service_internal_test.go @@ -15,15 +15,15 @@ import ( "github.com/fil-forge/versitygw/s3err" "github.com/gofiber/fiber/v3" - hiltclient "github.com/fil-forge/hilt/pkg/client" - hiltauth "github.com/fil-forge/hilt/pkg/rpc/service/auth" - "github.com/fil-forge/hilt/pkg/sigv4" "github.com/fil-forge/forge/ingot/internal/reqscope" "github.com/fil-forge/forge/ingot/registry" - contentcmds "github.com/fil-forge/libforge/commands/content" - s3 "github.com/fil-forge/libforge/commands/s3" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" - s3req "github.com/fil-forge/libforge/commands/s3/request" + hiltclient "github.com/fil-forge/forge/internal/client/hilt" + "github.com/fil-forge/forge/internal/sigv4" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + hiltauth "github.com/fil-forge/forge/protocol/commands/s3/request" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "github.com/fil-forge/ucantone/did" ucanerrors "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/multikey/ed25519" diff --git a/ingot/iam/service_test.go b/ingot/iam/service_test.go index fbb27ac3..32716437 100644 --- a/ingot/iam/service_test.go +++ b/ingot/iam/service_test.go @@ -7,9 +7,9 @@ import ( "net/http/httptest" "testing" - s3 "github.com/fil-forge/libforge/commands/s3" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" - s3req "github.com/fil-forge/libforge/commands/s3/request" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" @@ -18,9 +18,9 @@ import ( "github.com/gofiber/fiber/v3" "github.com/stretchr/testify/require" - hiltclient "github.com/fil-forge/hilt/pkg/client" "github.com/fil-forge/forge/ingot/iam" "github.com/fil-forge/forge/ingot/internal/reqscope" + hiltclient "github.com/fil-forge/forge/internal/client/hilt" ) // fakeAuthorizer records the request it authorized and returns canned diff --git a/ingot/inmem/segments_test.go b/ingot/inmem/segments_test.go index ec22dddc..44ae2e1f 100644 --- a/ingot/inmem/segments_test.go +++ b/ingot/inmem/segments_test.go @@ -9,7 +9,7 @@ import ( "github.com/fil-forge/forge/ingot/blockstore" "github.com/fil-forge/forge/ingot/registry" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" ) func testCid(t *testing.T, s string) cid.Cid { diff --git a/ingot/inmem/store.go b/ingot/inmem/store.go index 4c53749e..6a95b521 100644 --- a/ingot/inmem/store.go +++ b/ingot/inmem/store.go @@ -26,14 +26,14 @@ import ( "github.com/ipfs/go-cid" "github.com/multiformats/go-multihash" - "github.com/fil-forge/hilt/pkg/sigv4" "github.com/fil-forge/forge/ingot/blockstore" "github.com/fil-forge/forge/ingot/bucketauthority" "github.com/fil-forge/forge/ingot/logstore" "github.com/fil-forge/forge/ingot/registry" "github.com/fil-forge/forge/ingot/uploader" - "github.com/fil-forge/libforge/commands/s3" - "github.com/fil-forge/libforge/commands/s3/bucket" + "github.com/fil-forge/forge/internal/sigv4" + "github.com/fil-forge/forge/protocol/commands/s3" + "github.com/fil-forge/forge/protocol/commands/s3/bucket" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey/ed25519" ) diff --git a/ingot/inmem/store_test.go b/ingot/inmem/store_test.go index ea45b5ac..d245a28d 100644 --- a/ingot/inmem/store_test.go +++ b/ingot/inmem/store_test.go @@ -5,9 +5,9 @@ import ( "net/http" "testing" - s3 "github.com/fil-forge/libforge/commands/s3" - "github.com/fil-forge/libforge/commands/s3/bucket" - "github.com/fil-forge/libforge/testutil" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + "github.com/fil-forge/forge/protocol/commands/s3/bucket" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/forge/ingot/registry" ) diff --git a/ingot/inmem/stores_test.go b/ingot/inmem/stores_test.go index 33b6d701..6cf8f30e 100644 --- a/ingot/inmem/stores_test.go +++ b/ingot/inmem/stores_test.go @@ -10,8 +10,8 @@ import ( "time" "github.com/fil-forge/forge/ingot/registry" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" "github.com/multiformats/go-multihash" ) diff --git a/ingot/internal/fasthttputil/request.go b/ingot/internal/fasthttputil/request.go index 451930e6..470cf0ed 100644 --- a/ingot/internal/fasthttputil/request.go +++ b/ingot/internal/fasthttputil/request.go @@ -1,7 +1,7 @@ package fasthttputil import ( - "github.com/fil-forge/libforge/commands/s3" + "github.com/fil-forge/forge/protocol/commands/s3" "github.com/valyala/fasthttp" ) diff --git a/ingot/internal/reqscope/reqscope.go b/ingot/internal/reqscope/reqscope.go index 8576ef08..cc1b48a3 100644 --- a/ingot/internal/reqscope/reqscope.go +++ b/ingot/internal/reqscope/reqscope.go @@ -13,9 +13,9 @@ package reqscope import ( "context" - "github.com/fil-forge/libforge/commands/s3" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/protocol/commands/s3" "github.com/fil-forge/ucantone/did" + ucanlib "github.com/fil-forge/ucantone/ucanlib" ) // proofStoreContextKey is the private context / user-value key type, so it can't diff --git a/ingot/internal/ucanexec/execute.go b/ingot/internal/ucanexec/execute.go index f60731e9..a67ba442 100644 --- a/ingot/internal/ucanexec/execute.go +++ b/ingot/internal/ucanexec/execute.go @@ -5,7 +5,7 @@ // It is a trimmed, dependency-light copy of fil-forge/sprue's // pkg/lib/ucan_client.Execute: the executor is taken as an interface (so the // same helper drives both the HTTP client used for allocate/accept/publish and -// the libforge retrieval client used for ranged reads), and the sprue-internal +// the protocol retrieval client used for ranged reads), and the sprue-internal // structured logging (zapipld) has been dropped. package ucanexec diff --git a/ingot/module.go b/ingot/module.go index 3208eab2..30dcd40b 100644 --- a/ingot/module.go +++ b/ingot/module.go @@ -14,7 +14,7 @@ // - *zap.Logger // - *pgxpool.Pool — ingot owns its schema and runs its own goose // migrations against this pool at startup -// - identity.Identity — the agent (libforge identity); issuer of every +// - identity.Identity — the agent (internal/identity); issuer of every // outbound invocation (a did:key, or a did:web wrapping the key) // // Module manages the embedded S3 Server's lifecycle and provides nothing to the @@ -49,17 +49,16 @@ import ( "fmt" "net/url" - "github.com/fil-forge/libforge/identity" - "github.com/fil-forge/libforge/receipt" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/receipt" "github.com/fil-forge/ucantone/did" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/fil-forge/versitygw/auth" "github.com/jackc/pgx/v5/pgxpool" "github.com/openbao/openbao/api/v2" "go.uber.org/fx" "go.uber.org/zap" - hiltclient "github.com/fil-forge/hilt/pkg/client" "github.com/fil-forge/forge/ingot/blockstore" "github.com/fil-forge/forge/ingot/bucketauthority" "github.com/fil-forge/forge/ingot/config" @@ -73,6 +72,7 @@ import ( "github.com/fil-forge/forge/ingot/tenantkey" "github.com/fil-forge/forge/ingot/tokenstore" "github.com/fil-forge/forge/ingot/uploader" + hiltclient "github.com/fil-forge/forge/internal/client/hilt" swarfclient "github.com/fil-forge/swarf/pkg/client" ) @@ -163,7 +163,7 @@ type serverParams struct { Multipart registry.MultipartStore Parks registry.ParkStore Meta logstore.Meta - // Identity is the agent identity (the host-provided libforge identity, the + // Identity is the agent identity (the host-provided identity.Identity, the // issuer of every outbound invocation); the listener serves its DID // document at /.well-known/did.json. Required. Identity identity.Identity diff --git a/ingot/module_test.go b/ingot/module_test.go index 56e9632c..2c6d4885 100644 --- a/ingot/module_test.go +++ b/ingot/module_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/fil-forge/forge/ingot/config" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/jackc/pgx/v5/pgxpool" "go.uber.org/fx" diff --git a/ingot/regionkey/regionkey_test.go b/ingot/regionkey/regionkey_test.go index 0087aa9a..0798561a 100644 --- a/ingot/regionkey/regionkey_test.go +++ b/ingot/regionkey/regionkey_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/fil-forge/forge/ingot/regionkey" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/multiformats/go-multihash" "github.com/stretchr/testify/require" ) diff --git a/ingot/regionkey_provider_internal_test.go b/ingot/regionkey_provider_internal_test.go index 8aeb9b7b..dde984f4 100644 --- a/ingot/regionkey_provider_internal_test.go +++ b/ingot/regionkey_provider_internal_test.go @@ -5,7 +5,7 @@ import ( "encoding/base64" "testing" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/multiformats/go-multihash" "go.uber.org/zap" diff --git a/ingot/registry/locallocator.go b/ingot/registry/locallocator.go index 7d16bb3a..5d86e4cb 100644 --- a/ingot/registry/locallocator.go +++ b/ingot/registry/locallocator.go @@ -6,8 +6,8 @@ import ( "fmt" "net/url" - "github.com/fil-forge/libforge/blobindex" - "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/forge/protocol/commands" "github.com/fil-forge/ucantone/did" mh "github.com/multiformats/go-multihash" diff --git a/ingot/registry/postgres_live_test.go b/ingot/registry/postgres_live_test.go index 3cfa1575..d7e777bc 100644 --- a/ingot/registry/postgres_live_test.go +++ b/ingot/registry/postgres_live_test.go @@ -16,8 +16,8 @@ import ( "github.com/fil-forge/forge/ingot/blockstore" "github.com/fil-forge/forge/ingot/migrations" "github.com/fil-forge/forge/ingot/registry" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" ) func liveCid(t *testing.T, s string) cid.Cid { diff --git a/ingot/s3frontend/decrypt_test.go b/ingot/s3frontend/decrypt_test.go index 551e8dae..7381f7f2 100644 --- a/ingot/s3frontend/decrypt_test.go +++ b/ingot/s3frontend/decrypt_test.go @@ -10,8 +10,8 @@ import ( "strings" "testing" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" "github.com/filecoin-project/go-fee" "github.com/filecoin-project/go-fee/aesstream" "github.com/multiformats/go-multihash" diff --git a/ingot/s3frontend/upload_dedup_test.go b/ingot/s3frontend/upload_dedup_test.go index d599b5c0..81b1b547 100644 --- a/ingot/s3frontend/upload_dedup_test.go +++ b/ingot/s3frontend/upload_dedup_test.go @@ -14,8 +14,8 @@ import ( "github.com/fil-forge/forge/ingot/logstore" "github.com/fil-forge/forge/ingot/registry" "github.com/fil-forge/forge/ingot/uploader" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" ) // countingUploader records how many times each digest is uploaded. It returns diff --git a/ingot/server.go b/ingot/server.go index d8d86913..9e18658a 100644 --- a/ingot/server.go +++ b/ingot/server.go @@ -8,7 +8,7 @@ import ( "path/filepath" "time" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/did/web" "github.com/fil-forge/versitygw/auth" diff --git a/ingot/tokenstore/mem.go b/ingot/tokenstore/mem.go index 799933ed..4906c6dd 100644 --- a/ingot/tokenstore/mem.go +++ b/ingot/tokenstore/mem.go @@ -6,9 +6,9 @@ import ( "iter" "sync" - ucanlib "github.com/fil-forge/libforge/ucan" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" ) diff --git a/ingot/tokenstore/types.go b/ingot/tokenstore/types.go index dfab697e..becc410f 100644 --- a/ingot/tokenstore/types.go +++ b/ingot/tokenstore/types.go @@ -11,9 +11,9 @@ import ( "context" "iter" - ucanlib "github.com/fil-forge/libforge/ucan" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" + ucanlib "github.com/fil-forge/ucantone/ucanlib" ) // Store is a ucanlib.ProofStore that additionally accepts new diff --git a/ingot/uploader/blob.go b/ingot/uploader/blob.go index 79f80911..209d4d3b 100644 --- a/ingot/uploader/blob.go +++ b/ingot/uploader/blob.go @@ -6,9 +6,9 @@ import ( "fmt" "os" - assertcmds "github.com/fil-forge/libforge/commands/assert" - blobcmds "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/digestutil" + assertcmds "github.com/fil-forge/forge/protocol/commands/assert" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/ucantone/did" ucanerrors "github.com/fil-forge/ucantone/errors" "github.com/ipfs/go-cid" diff --git a/ingot/uploader/forge.go b/ingot/uploader/forge.go index 8632feef..196bde54 100644 --- a/ingot/uploader/forge.go +++ b/ingot/uploader/forge.go @@ -9,9 +9,9 @@ import ( "os" "time" - "github.com/fil-forge/libforge/blobindex" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/protocol/blobindex" "github.com/fil-forge/ucantone/did" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" "github.com/multiformats/go-multicodec" "github.com/multiformats/go-multihash" diff --git a/ingot/uploader/forge_test.go b/ingot/uploader/forge_test.go index 25962b97..ddf8a641 100644 --- a/ingot/uploader/forge_test.go +++ b/ingot/uploader/forge_test.go @@ -5,9 +5,9 @@ import ( "net/url" "testing" - ucanlib "github.com/fil-forge/libforge/ucan" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey/ed25519" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/stretchr/testify/require" "github.com/fil-forge/forge/ingot/forgeclient" diff --git a/internal/LICENSE.md b/internal/LICENSE.md new file mode 100644 index 00000000..8b486090 --- /dev/null +++ b/internal/LICENSE.md @@ -0,0 +1,232 @@ +The contents of this repository are Copyright (c) corresponding authors and +contributors, licensed under the `Permissive License Stack` meaning either of: + +- Apache-2.0 Software License: https://www.apache.org/licenses/LICENSE-2.0 + ([...4tr2kfsq](https://dweb.link/ipfs/bafkreiankqxazcae4onkp436wag2lj3ccso4nawxqkkfckd6cg4tr2kfsq)) + +- MIT Software License: https://opensource.org/licenses/MIT + ([...vljevcba](https://dweb.link/ipfs/bafkreiepofszg4gfe2gzuhojmksgemsub2h4uy2gewdnr35kswvljevcba)) + +You may not use the contents of this repository except in compliance +with one of the listed Licenses. For an extended clarification of the +intent behind the choice of Licensing please refer to +https://protocol.ai/blog/announcing-the-permissive-license-stack/ + +Unless required by applicable law or agreed to in writing, software +distributed under the terms listed in this notice is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See each License for the specific language +governing permissions and limitations under that License. + + + +`SPDX-License-Identifier: Apache-2.0 OR MIT` + +Verbatim copies of both licenses are included below: + +
Apache-2.0 Software License + +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +``` + +
+ +
MIT Software License + +``` +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` + +
diff --git a/internal/client/delegator/client.go b/internal/client/delegator/client.go new file mode 100644 index 00000000..6845f2d0 --- /dev/null +++ b/internal/client/delegator/client.go @@ -0,0 +1,248 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/fil-forge/ucantone/did" +) + +type Client struct { + baseURL string + httpClient *http.Client +} + +func New(baseURL string) (*Client, error) { + _, err := url.Parse(baseURL) + if err != nil { + return nil, fmt.Errorf("invalid base URL: %w", err) + } + + return &Client{ + baseURL: baseURL, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + }, nil +} + +func (c *Client) WithHTTPClient(httpClient *http.Client) *Client { + c.httpClient = httpClient + return c +} + +type RequestApprovalRequest struct { + Operator string `json:"operator"` + OwnerAddress string `json:"owner_address"` + Signature []byte `json:"signature"` +} + +func (c *Client) RequestApproval(ctx context.Context, req *RequestApprovalRequest) error { + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/registrar/request-approval", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + var errResp map[string]string + if err := json.NewDecoder(resp.Body).Decode(&errResp); err == nil { + if errMsg, ok := errResp["error"]; ok { + return fmt.Errorf("registration failed: %s", errMsg) + } + } + return fmt.Errorf("registration failed with status: %d", resp.StatusCode) + } + + return nil +} + +type RegisterRequest struct { + Operator string `json:"operator"` + OwnerAddress string `json:"owner_address"` + ProofSetID uint64 `json:"proof_set_id"` + OperatorEmail string `json:"operator_email"` + PublicURL string `json:"public_url"` + // Proofs is a UCAN container with delegations to upload service for + // `/blob/allocate`, `/blob/accept`, `/blob/replica/allocate` and `/pdp/info` + // capabilities on the registering provider. + Proofs string `json:"proofs"` +} + +func (c *Client) Register(ctx context.Context, req *RegisterRequest) error { + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal request: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, c.baseURL+"/registrar/register-node", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + var errResp map[string]string + if err := json.NewDecoder(resp.Body).Decode(&errResp); err == nil { + if errMsg, ok := errResp["error"]; ok { + return fmt.Errorf("registration failed: %s", errMsg) + } + } + return fmt.Errorf("registration failed with status: %d", resp.StatusCode) + } + + return nil +} + +type IsRegisteredRequest struct { + DID string `json:"did"` +} + +func (c *Client) IsRegistered(ctx context.Context, req *IsRegisteredRequest) (bool, error) { + body, err := json.Marshal(req) + if err != nil { + return false, fmt.Errorf("failed to marshal request: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/registrar/is-registered", bytes.NewReader(body)) + if err != nil { + return false, fmt.Errorf("failed to create request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return false, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusNotFound: + return false, nil + default: + var errResp map[string]string + if err := json.NewDecoder(resp.Body).Decode(&errResp); err == nil { + if errMsg, ok := errResp["error"]; ok { + return false, fmt.Errorf("check registration failed: %s", errMsg) + } + } + return false, fmt.Errorf("check registration failed with status: %d", resp.StatusCode) + } +} + +type RequestProofsRequest struct { + DID string `json:"did"` +} + +type RequestProofsResponse struct { + Proofs Proofs `json:"proofs"` +} + +type Proofs struct { + Indexer []byte `json:"indexer"` + EgressTracker []byte `json:"egress_tracker"` +} + +func (c *Client) RequestProofs(ctx context.Context, did string) (*RequestProofsResponse, error) { + req := &RequestProofsRequest{DID: did} + body, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/registrar/request-proofs", bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var errResp map[string]string + if err := json.NewDecoder(resp.Body).Decode(&errResp); err == nil { + if errMsg, ok := errResp["error"]; ok { + return nil, fmt.Errorf("request proof failed: %s", errMsg) + } + } + return nil, fmt.Errorf("request proof failed with status: %d", resp.StatusCode) + } + + var result RequestProofsResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} + +func (c *Client) HealthCheck(ctx context.Context) error { + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/healthcheck", nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("health check failed with status: %d", resp.StatusCode) + } + + return nil +} + +func (c *Client) DIDDocument(ctx context.Context) (*did.Document, error) { + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/.well-known/did.json", nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to send request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("get did document failed with status: %d", resp.StatusCode) + } + + var result did.Document + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} diff --git a/hilt/pkg/client/client.go b/internal/client/hilt/client.go similarity index 95% rename from hilt/pkg/client/client.go rename to internal/client/hilt/client.go index 6fc605ae..d253d270 100644 --- a/hilt/pkg/client/client.go +++ b/internal/client/hilt/client.go @@ -1,4 +1,4 @@ -package client +package hilt import ( "context" @@ -6,11 +6,10 @@ import ( "net/http" "net/url" - "github.com/fil-forge/forge/hilt/pkg/lib/zapucan" - s3 "github.com/fil-forge/libforge/commands/s3" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" - s3req "github.com/fil-forge/libforge/commands/s3/request" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/internal/zapucan" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" @@ -18,6 +17,7 @@ import ( "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" "github.com/fil-forge/ucantone/ucan/invocation" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "go.uber.org/zap" ) diff --git a/hilt/pkg/client/client_test.go b/internal/client/hilt/client_test.go similarity index 89% rename from hilt/pkg/client/client_test.go rename to internal/client/hilt/client_test.go index 5cd62fee..ffb70412 100644 --- a/hilt/pkg/client/client_test.go +++ b/internal/client/hilt/client_test.go @@ -1,4 +1,4 @@ -package client_test +package hilt_test import ( "context" @@ -7,28 +7,28 @@ import ( "net/url" "testing" - "github.com/fil-forge/forge/hilt/pkg/client" - s3 "github.com/fil-forge/libforge/commands/s3" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" - s3req "github.com/fil-forge/libforge/commands/s3/request" - "github.com/fil-forge/libforge/testutil" - ucanlib "github.com/fil-forge/libforge/ucan" + hiltclient "github.com/fil-forge/forge/internal/client/hilt" + s3 "github.com/fil-forge/forge/protocol/commands/s3" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" ) // newHiltClient builds a Client whose transport is the given in-process server, // exercising New itself. -func newHiltClient(t *testing.T, hilt ucan.Issuer, srv *server.HTTPServer, issuer ucan.Issuer, proofs ucanlib.ProofStore) *client.Client { +func newHiltClient(t *testing.T, hilt ucan.Issuer, srv *server.HTTPServer, issuer ucan.Issuer, proofs ucanlib.ProofStore) *hiltclient.Client { t.Helper() u, err := url.Parse("http://hilt.test") require.NoError(t, err) - c, err := client.New(hilt.DID(), *u, issuer, client.WithBaseProofs(proofs), client.WithHTTPClient(&http.Client{Transport: srv})) + c, err := hiltclient.New(hilt.DID(), *u, issuer, hiltclient.WithBaseProofs(proofs), hiltclient.WithHTTPClient(&http.Client{Transport: srv})) require.NoError(t, err) return c } @@ -199,7 +199,7 @@ func TestClientErrors(t *testing.T) { t.Run("proof chain error", func(t *testing.T) { srv := server.NewHTTP(hilt) c := newHiltClient(t, hilt, srv, ingot, nil) - _, _, err := c.AuthorizeRequest(t.Context(), req, client.WithProofs(errProofStore{err: errors.New("boom")})) + _, _, err := c.AuthorizeRequest(t.Context(), req, hiltclient.WithProofs(errProofStore{err: errors.New("boom")})) require.Error(t, err) require.Contains(t, err.Error(), "getting proof chain") }) @@ -207,8 +207,8 @@ func TestClientErrors(t *testing.T) { t.Run("execution error", func(t *testing.T) { u, err := url.Parse("http://hilt.test") require.NoError(t, err) - c, err := client.New(hilt.DID(), *u, ingot, client.WithBaseProofs(rootProofs(t, s3req.Authorize, hilt, ingot.DID())), - client.WithHTTPClient(&http.Client{Transport: errRoundTripper{}})) + c, err := hiltclient.New(hilt.DID(), *u, ingot, hiltclient.WithBaseProofs(rootProofs(t, s3req.Authorize, hilt, ingot.DID())), + hiltclient.WithHTTPClient(&http.Client{Transport: errRoundTripper{}})) require.NoError(t, err) _, _, err = c.AuthorizeRequest(t.Context(), req) require.Error(t, err) diff --git a/internal/go.mod b/internal/go.mod new file mode 100644 index 00000000..4802228e --- /dev/null +++ b/internal/go.mod @@ -0,0 +1,68 @@ +module github.com/fil-forge/forge/internal + +go 1.27.0 + +require ( + github.com/ethereum/go-ethereum v1.16.7 + github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 + github.com/fil-forge/forge/protocol v0.0.0 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 + github.com/filecoin-project/go-fil-commcid v0.3.1 + github.com/filecoin-project/go-fil-commp-hashhash v0.4.0 + github.com/ipfs/go-cid v0.6.2 + github.com/ipni/go-libipni v0.8.2 + github.com/libp2p/go-libp2p v0.49.0 + github.com/multiformats/go-multiaddr v0.16.1 + github.com/multiformats/go-multicodec v0.10.0 + github.com/multiformats/go-multihash v0.2.3 + github.com/multiformats/go-varint v0.1.0 + github.com/stretchr/testify v1.12.1 + go.uber.org/zap v1.28.0 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/alanshaw/dag-json-gen v0.0.9 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multibase v0.3.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/protobuf v1.36.11 // indirect + lukechampine.com/blake3 v1.4.1 // indirect + pitr.ca/jsontokenizer v0.3.2 // indirect +) + +replace github.com/fil-forge/forge/protocol => ../protocol diff --git a/internal/go.sum b/internal/go.sum new file mode 100644 index 00000000..38fcab79 --- /dev/null +++ b/internal/go.sum @@ -0,0 +1,269 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +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/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/alanshaw/dag-json-gen v0.0.9 h1:q59Ra1mQB6HnFGI1ilFxsZvF1CqGN0XDlUWq16EJ6pw= +github.com/alanshaw/dag-json-gen v0.0.9/go.mod h1:v1YBZcS4B355MqxtyQr+fGNbEhm0CzHd+gOqOO/MZ+I= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +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/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= +github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 h1:h7SZVboCO1iPDtdxPJjs1fj4IVFq/dpDWFevkXtwgps= +github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390/go.mod h1:ur5fRrG8v9twwwRFdX7lquGza4liT6x/xAFW7T4XVIc= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +github.com/filecoin-project/go-fil-commcid v0.3.1 h1:4EfxpHSlvtkOqa9weG2Yt5kxFmPib2xU7Uc9Lbqk7fs= +github.com/filecoin-project/go-fil-commcid v0.3.1/go.mod h1:z7Ssf8d7kspF9QRAVHDbZ+43JK4mkhbGH5lyph1TnKY= +github.com/filecoin-project/go-fil-commp-hashhash v0.4.0 h1:wmIYOYQmvQHBrilk4qP6UuYrBxH2ojLiBi8eavRjmG8= +github.com/filecoin-project/go-fil-commp-hashhash v0.4.0/go.mod h1:PH/zp1sbBNSA7vIsrVFB5oxPBoZWGPpd5/emulOAX5s= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +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.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= +github.com/ipni/go-libipni v0.8.2 h1:mRJe602pD3WKcl/2WJFT6CjdI181/DcThqEdRL0usA8= +github.com/ipni/go-libipni v0.8.2/go.mod h1:YlujVRDn3uksrYeV62ERtqKQdRVgbnjF13wPsnCCHAk= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/libp2p/go-libp2p v0.49.0 h1:ibXuYPIHmMIPShob1BktQvSuFQkq/MemhQOLKfGujjw= +github.com/libp2p/go-libp2p v0.49.0/go.mod h1:lzjVcOBk5fCn1QD2XbSOKLZesB6gEsry8SLjCsAAGT4= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +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/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/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/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= +github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +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-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +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/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/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/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +pitr.ca/jsontokenizer v0.3.2 h1:kzsKwfkWPV5XCmAF//hznh3L9IXbiL8aHbI0FswRkCs= +pitr.ca/jsontokenizer v0.3.2/go.mod h1:3DJdA2QNOU6cI0XkH6pRKZ4Oe8G5SDRUQ6PFAwaQ3YY= diff --git a/internal/identity/identity.go b/internal/identity/identity.go new file mode 100644 index 00000000..ef2a2b7f --- /dev/null +++ b/internal/identity/identity.go @@ -0,0 +1,124 @@ +package identity + +import ( + "fmt" + "os" + + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/multikey" + "github.com/fil-forge/ucantone/multikey/ed25519" +) + +// Identity holds a service's cryptographic identity. It's intended to be held +// by the service itself. This is the source of its DID document, can then be +// published (eg, to the web). Other services should use normal DID resolution +// to find the document. +type Identity struct { + multikey.Issuer +} + +// New creates a new identity. If privateKeyBase64 is empty, generates a new +// key. If serviceDID is empty, uses the key DID derived from the key. +func New(privateKeyBase64 string, serviceDID string) (Identity, error) { + var signer multikey.Signer + var issuer multikey.Issuer + var err error + + if privateKeyBase64 == "" { + // Generate ephemeral identity + signer, err = ed25519.Generate() + if err != nil { + return Identity{}, fmt.Errorf("failed to generate signer: %w", err) + } + } else { + // Decode provided key + signer, err = ed25519.Parse(privateKeyBase64) + if err != nil { + return Identity{}, fmt.Errorf("failed to create signer from key: %w", err) + } + } + + if serviceDID == "" { + issuer = multikey.KeyIssuer(signer) + } else { + d, err := did.Parse(serviceDID) + if err != nil { + return Identity{}, fmt.Errorf("failed to parse service DID %q: %w", serviceDID, err) + } + issuer = multikey.NewIssuer(d, signer) + } + + return Identity{Issuer: issuer}, nil +} + +// DIDDocument returns the identity's DID document. This should be available for +// other services performing did:web resolution. This enables other services to +// verify signatures from this service. +func (i Identity) DIDDocument() (did.Document, error) { + doc := did.NewDocument(i.DID()) + + // Can only derive a verification method from a multikey verifier. This could + // be extended in the future. + mkVerifier, ok := i.Verifier().(multikey.Verifier) + if !ok { + return did.Document{}, fmt.Errorf("identity does not have a multikey verifier") + } + // Fragment expects a bare name and prepends the "#" itself; passing "#key-0" + // would serialize as "#%23key-0". + vm := multikey.DeriveVerificationMethod(doc.Fragment("key-0"), mkVerifier) + vm.Controller = doc.ID + + if err := doc.VerificationMethods.Add(vm); err != nil { + return did.Document{}, err + } + + if err := doc.Authentication.Add(vm); err != nil { + return did.Document{}, err + } + if err := doc.AssertionMethod.Add(vm); err != nil { + return did.Document{}, err + } + if err := doc.CapabilityDelegation.Add(vm); err != nil { + return did.Document{}, err + } + if err := doc.CapabilityInvocation.Add(vm); err != nil { + return did.Document{}, err + } + + return doc, nil +} + +// NewFromPEMFile creates a new identity from an Ed25519 PEM key file. +func NewFromPEMFile(keyFilePath string) (Identity, error) { + pem, err := os.ReadFile(keyFilePath) + if err != nil { + return Identity{}, fmt.Errorf("failed to read key file: %w", err) + } + keySigner, err := DecodeSignerFromPEM(pem) + if err != nil { + return Identity{}, fmt.Errorf("failed to decode key from PEM file: %w", err) + } + return Identity{Issuer: multikey.KeyIssuer(keySigner)}, nil +} + +// NewFromPEMFileWithDID creates a new identity from an Ed25519 PEM key file. +// When serviceDID is provided (e.g., "did:web:upload"), the identity will use +// that DID. Otherwise, it will use the key DID derived from the key. +func NewFromPEMFileWithDID(keyFilePath string, serviceDID string) (Identity, error) { + keyId, err := NewFromPEMFile(keyFilePath) + if err != nil { + return Identity{}, fmt.Errorf("creating identity from PEM file: %w", err) + } + + // If serviceDID is provided, wrap the signer with the did:web identity + if serviceDID != "" { + d, err := did.Parse(serviceDID) + if err != nil { + return Identity{}, fmt.Errorf("failed to parse service DID %q: %w", serviceDID, err) + } + + return Identity{Issuer: multikey.NewIssuer(d, keyId)}, nil + } + + return Identity{Issuer: keyId.Issuer}, nil +} diff --git a/internal/identity/identity_test.go b/internal/identity/identity_test.go new file mode 100644 index 00000000..af7ee92f --- /dev/null +++ b/internal/identity/identity_test.go @@ -0,0 +1,40 @@ +package identity_test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/fil-forge/forge/internal/identity" + "github.com/stretchr/testify/require" +) + +func TestDIDDocument(t *testing.T) { + const serviceDID = "did:web:example.com" + + id, err := identity.New("", serviceDID) + require.NoError(t, err) + + doc, err := id.DIDDocument() + require.NoError(t, err) + + docJSON, err := json.Marshal(doc) + require.NoError(t, err) + + var parsed struct { + ID string `json:"id"` + VerificationMethod []struct { + ID string `json:"id"` + Controller string `json:"controller"` + } `json:"verificationMethod"` + } + require.NoError(t, json.Unmarshal(docJSON, &parsed)) + + require.Equal(t, serviceDID, parsed.ID) + require.Len(t, parsed.VerificationMethod, 1) + + vm := parsed.VerificationMethod[0] + require.Equal(t, serviceDID+"#key-0", vm.ID) + require.False(t, strings.Contains(vm.ID, "%23"), "verification method ID must not contain a percent-encoded '#'") + require.Equal(t, serviceDID, vm.Controller) +} diff --git a/internal/identity/pem.go b/internal/identity/pem.go new file mode 100644 index 00000000..bee6bee6 --- /dev/null +++ b/internal/identity/pem.go @@ -0,0 +1,67 @@ +package identity + +import ( + "bytes" + crypto_ed25519 "crypto/ed25519" + "crypto/x509" + "encoding/pem" + "fmt" + + "github.com/fil-forge/ucantone/multikey" + "github.com/fil-forge/ucantone/multikey/ed25519" +) + +// EncodeSignerToPEM encodes a signer to a PKCS#8 PEM format. The signer's key +// should be of a type supported by ["crypto/x509".MarshalPKCS8PrivateKey]. +func EncodeSignerToPEM(signer multikey.Signer) ([]byte, error) { + privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(signer.PrivateKey()) + if err != nil { + return nil, fmt.Errorf("marshaling private key of signer %s: %w", signer, err) + } + + privateKeyBlock := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: privateKeyBytes, + } + + buffer := new(bytes.Buffer) + if err := pem.Encode(buffer, privateKeyBlock); err != nil { + return nil, fmt.Errorf("encoding private key of signer %s: %w", signer, err) + } + + return buffer.Bytes(), nil +} + +// DecodeSignerFromPEM loads a private key from a PKCS#8 PEM as a signer. +// Currently, only Ed25519 keys are supported. +func DecodeSignerFromPEM(pemData []byte) (multikey.Signer, error) { + var privateKey *crypto_ed25519.PrivateKey + rest := pemData + for { + block, remaining := pem.Decode(rest) + if block == nil { + break + } + rest = remaining + + if block.Type == "PRIVATE KEY" { + parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing PKCS#8 private key: %w", err) + } + + key, ok := parsedKey.(crypto_ed25519.PrivateKey) + if !ok { + return nil, fmt.Errorf("key is not an Ed25519 private key") + } + privateKey = &key + break + } + } + + if privateKey == nil { + return nil, fmt.Errorf("no PRIVATE KEY block found in PEM file") + } + + return ed25519.FromRaw(privateKey.Seed()) +} diff --git a/internal/identity/pem_test.go b/internal/identity/pem_test.go new file mode 100644 index 00000000..3d9b05e2 --- /dev/null +++ b/internal/identity/pem_test.go @@ -0,0 +1,36 @@ +package identity_test + +import ( + "testing" + + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/ucantone/multikey/ed25519" + "github.com/stretchr/testify/require" +) + +func TestEd25519SignerPEMRoundTrip(t *testing.T) { + original, err := ed25519.Generate() + require.NoError(t, err) + + pemBytes, err := identity.EncodeSignerToPEM(original) + require.NoError(t, err) + require.NotEmpty(t, pemBytes) + + decoded, err := identity.DecodeSignerFromPEM(pemBytes) + require.NoError(t, err) + + require.Equal(t, original.Raw(), decoded.Raw()) + require.Equal(t, original.Bytes(), decoded.Bytes()) + require.Equal(t, original.KeyDID(), decoded.KeyDID()) +} + +func TestDecodeEd25519SignerFromPEM_NoPrivateKeyBlock(t *testing.T) { + pemData := []byte("-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n") + _, err := identity.DecodeSignerFromPEM(pemData) + require.ErrorContains(t, err, "no PRIVATE KEY block found") +} + +func TestDecodeEd25519SignerFromPEM_Empty(t *testing.T) { + _, err := identity.DecodeSignerFromPEM(nil) + require.ErrorContains(t, err, "no PRIVATE KEY block found") +} diff --git a/internal/ipni/advertisement/advertisement.go b/internal/ipni/advertisement/advertisement.go new file mode 100644 index 00000000..7a15fe4d --- /dev/null +++ b/internal/ipni/advertisement/advertisement.go @@ -0,0 +1,109 @@ +package advertisement + +import ( + "bytes" + "fmt" + "net/url" + "regexp" + "strings" + + "github.com/fil-forge/ucantone/did" + "github.com/ipfs/go-cid" + "github.com/ipni/go-libipni/maurl" + "github.com/libp2p/go-libp2p/core/peer" + mh "github.com/multiformats/go-multihash" + + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/digestutil" +) + +const ( + BlobUrlPlaceholder = "{blob}" + BlobCIDUrlPlaceholder = "{blobCID}" +) + +// Encode canonically encodes ContextID data. +func EncodeContextID(space did.DID, digest mh.Multihash) ([]byte, error) { + return mh.Sum(bytes.Join([][]byte{[]byte(space.String()), digest}, nil), mh.SHA2_256, -1) +} + +// ShardCID extracts an alternate shard CID from the provider & location URLs in a location claim +func ShardCID(provider peer.AddrInfo, caveats assert.LocationArguments) (*cid.Cid, error) { + + // analyze each provider address + for _, addr := range provider.Addrs { + // first, attempt to convert the addr to a url scheme + url, err := maurl.ToURL(addr) + // if it can't be converted, skip + if err != nil { + continue + } + // must be an http url + if !(url.Scheme == "http" || url.Scheme == "https") { + continue + } + // if it does not have replaceable components in a location url, skip + if !strings.Contains(url.Path, BlobUrlPlaceholder) && !strings.Contains(url.Path, BlobCIDUrlPlaceholder) { + continue + } + // generate a regex to capture matching components of the url + urlRegex, err := urlToRegex(url) + if err != nil { + return nil, fmt.Errorf("parsing url regex: %w", err) + } + // go through each location in the claim + for _, location := range caveats.Location { + // if the location does not match the url regex, skip + if !urlRegex.MatchString(location.URL().String()) { + continue + } + // get matching components of the location + matches := urlRegex.FindStringSubmatch(location.URL().String()) + var blob mh.Multihash + // check for matches with the blob multihash + if urlRegex.SubexpIndex("blob") != -1 { + blob, err = digestutil.Parse(matches[urlRegex.SubexpIndex("blob")]) + if err != nil { + return nil, fmt.Errorf("location format has invalid multihash: %w", err) + } + // if no blobCID, just use the multihash + if urlRegex.SubexpIndex("blobCID") == -1 { + // if location hash matches hash in the url there's no need to save a shard + if caveats.Content.String() == blob.String() { + return nil, nil + } + shard := cid.NewCidV1(cid.Raw, blob) + return &shard, nil + } + } + + // check for matches with the blob cid + blobCID, err := cid.Decode(matches[urlRegex.SubexpIndex("blobCID")]) + if err != nil { + return nil, fmt.Errorf("location format has invalid cid: %w", err) + } + + // if there are multihash matches and cid matches they should be equal + if blob != nil && blob.String() != blobCID.Hash().String() { + return nil, fmt.Errorf("location format has both cid and multihash but they do not match") + } + + // if location hash matches the cid in the URL, just use that + if cid.NewCidV1(cid.Raw, caveats.Content).Equals(blobCID) { + return nil, nil + } + + return &blobCID, nil + } + } + return nil, nil +} + +func urlToRegex(u *url.URL) (*regexp.Regexp, error) { + escapedBlobString := url.PathEscape(BlobUrlPlaceholder) + escapedBlobCIDString := url.PathEscape(BlobCIDUrlPlaceholder) + + regexPattern := strings.ReplaceAll(regexp.QuoteMeta(u.String()), escapedBlobString, `(?P.+?)`) + regexPattern = strings.ReplaceAll(regexPattern, escapedBlobCIDString, `(?P.+?)`) + return regexp.Compile("^" + regexPattern + "$") +} diff --git a/internal/ipni/advertisement/advertisement_test.go b/internal/ipni/advertisement/advertisement_test.go new file mode 100644 index 00000000..72585e4f --- /dev/null +++ b/internal/ipni/advertisement/advertisement_test.go @@ -0,0 +1,150 @@ +package advertisement_test + +import ( + "net/url" + "testing" + + "github.com/ipfs/go-cid" + "github.com/ipni/go-libipni/maurl" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/digestutil" + "github.com/fil-forge/ucantone/testutil" + + "github.com/fil-forge/forge/internal/ipni/advertisement" +) + +func TestShardCID(t *testing.T) { + curioPath := testutil.Must(multiaddr.NewMultiaddr("/http-path/" + url.PathEscape("piece/{blobCID}")))(t) + blobPath := testutil.Must(multiaddr.NewMultiaddr("/http-path/" + url.PathEscape("blob/{blob}/{blob}")))(t) + mixedPath := testutil.Must(multiaddr.NewMultiaddr("/http-path/" + url.PathEscape("blob/{blobCID}/{blob}")))(t) + baseUrl := testutil.Must(url.Parse("https://node.com"))(t) + base := testutil.Must(maurl.FromURL(baseUrl))(t) + testCid := testutil.RandomCID(t) + testMhs := randomMultihashes(t, 3) + + testPieceLink := testutil.RandomCID(t) + + tests := []struct { + name string + provider peer.AddrInfo + caveats assert.LocationArguments + expected *cid.Cid + expectErr bool + }{ + { + name: "Valid shard CID from location url", + provider: peer.AddrInfo{ + ID: randomPeer(t), + Addrs: []multiaddr.Multiaddr{ + multiaddr.Join(base, curioPath), + }, + }, + caveats: assert.LocationArguments{ + Content: testMhs[0], + Location: []commands.CborURL{ + commands.CborURL(*baseUrl.JoinPath("piece", testPieceLink.String())), + }, + }, + expected: func() *cid.Cid { + c := testPieceLink + return &c + }(), + expectErr: false, + }, + { + name: "Valid shard multihash from location url", + provider: peer.AddrInfo{ + ID: randomPeer(t), + Addrs: []multiaddr.Multiaddr{ + multiaddr.Join(base, blobPath), + }, + }, + caveats: assert.LocationArguments{ + Content: testMhs[0], + Location: []commands.CborURL{ + commands.CborURL(*baseUrl.JoinPath("blob", digestutil.Format(testMhs[1]), digestutil.Format(testMhs[1]))), + }, + }, + expected: func() *cid.Cid { + cid := cid.NewCidV1(cid.Raw, testMhs[1]) + return &cid + }(), + expectErr: false, + }, + { + name: "Valid CID in mixed location url", + provider: peer.AddrInfo{ + ID: randomPeer(t), + Addrs: []multiaddr.Multiaddr{ + multiaddr.Join(base, mixedPath), + }, + }, + caveats: assert.LocationArguments{ + Content: testMhs[0], + Location: []commands.CborURL{ + commands.CborURL(*baseUrl.JoinPath("blob", testCid.String(), digestutil.Format(testCid.Hash()))), + }, + }, + expected: func() *cid.Cid { + c := testCid + return &c + }(), + expectErr: false, + }, + // TODO: test for error on blob & cid not matching + { + name: "No matching location", + provider: peer.AddrInfo{ + Addrs: []multiaddr.Multiaddr{ + multiaddr.Join(base, curioPath), + }, + }, + caveats: assert.LocationArguments{ + Location: []commands.CborURL{ + commands.CborURL(*baseUrl.JoinPath("blob", digestutil.Format(testMhs[1]), digestutil.Format(testMhs[1]))), + }, + Content: testMhs[0], + }, + expected: nil, + expectErr: false, + }, + { + name: "Invalid CID in location", + provider: peer.AddrInfo{ + Addrs: []multiaddr.Multiaddr{ + multiaddr.Join(base, curioPath), + }, + }, + caveats: assert.LocationArguments{ + Location: []commands.CborURL{ + commands.CborURL(*baseUrl.JoinPath("piece", "applesauce")), + }, + Content: testMhs[0], + }, + expected: nil, + expectErr: true, + }, + // TODO: invalid multihash in location test + // TODO: multiaddrs with blobs test + + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := advertisement.ShardCID(tt.provider, tt.caveats) + if (err != nil) != tt.expectErr { + t.Fatalf("expected error: %v, got: %v", tt.expectErr, err) + } + if result != nil && tt.expected != nil && !result.Equals(*tt.expected) { + t.Errorf("expected: %v, got: %v", tt.expected, result) + } + if result == nil && tt.expected != nil { + t.Errorf("expected: %v, got: nil", tt.expected) + } + }) + } +} diff --git a/internal/ipni/advertisement/helpers_test.go b/internal/ipni/advertisement/helpers_test.go new file mode 100644 index 00000000..2f46da78 --- /dev/null +++ b/internal/ipni/advertisement/helpers_test.go @@ -0,0 +1,32 @@ +package advertisement_test + +import ( + "math/rand" + "testing" + + "github.com/fil-forge/ucantone/testutil" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +func randomMultihashes(t *testing.T, count int) []multihash.Multihash { + require.Greater(t, count, 0, "count must be greater than 0") + mhs := make([]multihash.Multihash, 0, count) + for range count { + mhs = append(mhs, testutil.RandomDigest(t)) + } + return mhs +} + +var seedSeq int64 + +// randomPeer derives a peer ID from a deterministic key sequence, so test +// output is stable across runs. +func randomPeer(t *testing.T) peer.ID { + src := rand.NewSource(seedSeq) + seedSeq++ + _, publicKey := testutil.Must2(crypto.GenerateEd25519Key(rand.New(src)))(t) + return testutil.Must(peer.IDFromPublicKey(publicKey))(t) +} diff --git a/internal/jobqueue/jobqueue.go b/internal/jobqueue/jobqueue.go new file mode 100644 index 00000000..6d7ba70f --- /dev/null +++ b/internal/jobqueue/jobqueue.go @@ -0,0 +1,265 @@ +package jobqueue + +import ( + "context" + "errors" + "sync" + "time" +) + +// ErrQueueShutdown means the queue is shutdown so the job could not be queued +var ErrQueueShutdown = errors.New("queue is shutdown") + +type ( + // Option modifies the config of a JobQueue + Option func(*config) + + // Handler handles jobs of the given type + Handler[Job any] interface { + isHandler() + } + + // JobQueue is asyncronous queue for jobs, that can be processed in parallel + // by the job queue's handler + JobQueue[Job any] struct { + *config + handler Handler[Job] + incoming chan quitOrJob + closed chan struct{} + closing chan struct{} + } + + config struct { + jobTimeout time.Duration + shutdownTimeout time.Duration + errorHandler func(error) + buffer int + concurrency int + } + + quitOrJob interface { + isQuitOrJob() + } + + job[Job any] struct { + j Job + } + quit struct{} + + singleHandler[Job any] struct { + handler func(ctx context.Context, j Job) error + } + multiHandler[Job any] struct { + handler func(ctx context.Context, jobs []Job) error + } +) + +// WithBuffer allows a set amount of jobs to be buffered even if all workers are busy +func WithBuffer(buffer int) Option { + return func(c *config) { + c.buffer = buffer + } +} + +// WithConcurrency sets the number of workers that will process jobs in +// parallel +func WithConcurrency(concurrency int) Option { + return func(c *config) { + c.concurrency = concurrency + } +} + +// WithErrorHandler uses the given error handler whenever a job errors while processing +func WithErrorHandler(errorHandler func(error)) Option { + return func(c *config) { + c.errorHandler = errorHandler + } +} + +// WithJobTimeout cancels the past into context to the job handler after the specified +// timeout +func WithJobTimeout(jobTimeout time.Duration) Option { + return func(c *config) { + c.jobTimeout = jobTimeout + } +} + +// WithShutdownTimeout sets the shutdown timeout. When the queue is shutdown, the +// context passed to all job handlers will cancel after the specified timeout +func WithShutdownTimeout(shutdownTimeout time.Duration) Option { + return func(c *config) { + c.shutdownTimeout = shutdownTimeout + } +} + +func JobHandler[Job any](handler func(ctx context.Context, j Job) error) Handler[Job] { + return singleHandler[Job]{ + handler: handler, + } +} + +func MultiJobHandler[Job any](handler func(ctx context.Context, jobs []Job) error) Handler[Job] { + return multiHandler[Job]{ + handler: handler, + } +} + +// NewJobQueue returns a new job queue that processes with the given handler +func NewJobQueue[Job any](handler Handler[Job], opts ...Option) *JobQueue[Job] { + c := &config{ + concurrency: 1, + } + for _, opt := range opts { + opt(c) + } + return &JobQueue[Job]{ + config: c, + handler: handler, + incoming: make(chan quitOrJob), + closing: make(chan struct{}), + closed: make(chan struct{}), + } +} + +// Queue attempts to queue the job. It will fail if the queue is shutdown, or +// the passed context cancels before the job can be queued +func (p *JobQueue[Job]) Queue(ctx context.Context, j Job) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-p.closing: + return ErrQueueShutdown + case p.incoming <- job[Job]{j}: + return nil + } +} + +// Startup starts the queue in the background (returns immediately) +func (p *JobQueue[Job]) Startup() { + go p.run() +} + +// Shutdown shuts down the queue, returning when the whole queue is shutdown or +// the passed context cancels +func (p *JobQueue[Job]) Shutdown(ctx context.Context) error { + // signal the queue is closing -- this will cause anyone awaiting a queue + // to abort + close(p.closing) + // now get get a quit message into the incoming queue this will be the last + // message written in the queue but we also don't just close incoming cause it + // would cause a potential panic + p.incoming <- quit{} + // now wait for the go routines to complete + select { + case <-p.closed: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +func (p *JobQueue[Job]) run() { + // the queue is fully closed when this function completes + defer close(p.closed) + // outgoing will be used to consume jobs by the workers + outgoing := make(chan Job, p.buffer) + + // setup a cancellable context so that you can shut down all job + // executions when ready + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wg sync.WaitGroup + // spin up all workers + for range p.concurrency { + wg.Add(1) + go func() { + defer wg.Done() + p.worker(ctx, outgoing) + }() + } + + for { + // read the next message from the incoming queue + queued := <-p.incoming + switch typed := queued.(type) { + case job[Job]: + // if its a job, just send to the workers + outgoing <- typed.j + case quit: + // if it's a quit message, this is the last message we will receive + // so start the shutdown process + + // tell all the workers they're done processing jobs + close(outgoing) + // if there is a shut down timeout, queue a background routune to cancel + // the context (i.e. accelerate workers shutting down by the handler getting + // a shutdown context) + if p.shutdownTimeout != 0 { + timer := time.NewTimer(p.shutdownTimeout) + go func() { + <-timer.C + cancel() + }() + } + // wait for the workers to shutdown + wg.Wait() + return + } + } +} + +func (p *JobQueue[Job]) jobCtx(ctx context.Context) (context.Context, context.CancelFunc) { + if p.jobTimeout != 0 { + return context.WithTimeout(ctx, p.jobTimeout) + } + return context.WithCancel(ctx) +} + +func (p *JobQueue[Job]) handleJob(ctx context.Context, job Job, handler func(ctx context.Context, j Job) error) { + ctx, cancel := p.jobCtx(ctx) + defer cancel() + err := handler(ctx, job) + if err != nil && p.errorHandler != nil { + p.errorHandler(err) + } +} + +func (p *JobQueue[Job]) handleJobs(ctx context.Context, jobs []Job, handler func(ctx context.Context, jobs []Job) error) { + ctx, cancel := p.jobCtx(ctx) + defer cancel() + err := handler(ctx, jobs) + if err != nil && p.errorHandler != nil { + p.errorHandler(err) + } +} + +func (p *JobQueue[Job]) worker(ctx context.Context, jobs <-chan Job) { + for job := range jobs { + switch handler := p.handler.(type) { + case singleHandler[Job]: + p.handleJob(ctx, job, handler.handler) + case multiHandler[Job]: + toProcess := []Job{job} + readAvailable: + for { + select { + case job, ok := <-jobs: + if !ok { + break readAvailable + } + toProcess = append(toProcess, job) + default: + break readAvailable + } + } + p.handleJobs(ctx, toProcess, handler.handler) + } + } +} + +func (job[Job]) isQuitOrJob() {} +func (quit) isQuitOrJob() {} + +func (singleHandler[Job]) isHandler() {} +func (multiHandler[Job]) isHandler() {} diff --git a/internal/jobqueue/jobqueue_test.go b/internal/jobqueue/jobqueue_test.go new file mode 100644 index 00000000..3f4523c2 --- /dev/null +++ b/internal/jobqueue/jobqueue_test.go @@ -0,0 +1,238 @@ +package jobqueue_test + +import ( + "context" + "slices" + "sync" + "testing" + "time" + + "github.com/fil-forge/forge/internal/jobqueue" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Enqueues three jobs into a single-handler queue and verifies that all are processed before shutdown. +func TestJobQueueSingleHandlerBasic(t *testing.T) { + var mu sync.Mutex + processed := make([]int, 0) + + // Single handler that appends each job to a slice under a lock. + h := jobqueue.JobHandler(func(ctx context.Context, job int) error { + mu.Lock() + defer mu.Unlock() + processed = append(processed, job) + return nil + }) + + q := jobqueue.NewJobQueue[int](h) + q.Startup() + + ctx := context.Background() + require.NoError(t, q.Queue(ctx, 1)) + require.NoError(t, q.Queue(ctx, 2)) + require.NoError(t, q.Queue(ctx, 3)) + + shutdownCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + require.NoError(t, q.Shutdown(shutdownCtx)) + + // Check that all jobs have been processed + assert.ElementsMatch(t, []int{1, 2, 3}, processed) +} + +// Exercises concurrency by queueing multiple jobs in parallel with a concurrency level of 4. Verifies all jobs are handled +func TestJobQueueSingleHandlerConcurrency(t *testing.T) { + var mu sync.Mutex + var processed []int + + h := jobqueue.JobHandler(func(ctx context.Context, j int) error { + // Simulate random processing time + time.Sleep(5 * time.Millisecond) + mu.Lock() + defer mu.Unlock() + processed = append(processed, j) + return nil + }) + + q := jobqueue.NewJobQueue[int](h, + jobqueue.WithConcurrency(4), + ) + + q.Startup() + + ctx := context.Background() + jobCount := 20 + for i := 1; i <= jobCount; i++ { + require.NoError(t, q.Queue(ctx, i)) + } + + shutdownCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + require.NoError(t, q.Shutdown(shutdownCtx)) + + // We expect all jobs to be processed + require.Len(t, processed, jobCount, "expected all jobs to be processed") +} + +// Uses a multi-handler that reads all available jobs into a slice. Verifies that all jobs are seen in total. +func TestJobQueueMultiHandlerBasicBatching(t *testing.T) { + var mu sync.Mutex + var allBatches [][]int + + // multi handler that collects all the items that come in a batch + mh := jobqueue.MultiJobHandler(func(ctx context.Context, jobs []int) error { + mu.Lock() + defer mu.Unlock() + copied := make([]int, len(jobs)) + copy(copied, jobs) + allBatches = append(allBatches, copied) + return nil + }) + + q := jobqueue.NewJobQueue[int](mh, jobqueue.WithConcurrency(1)) + q.Startup() + + ctx := context.Background() + for i := 1; i <= 5; i++ { + require.NoError(t, q.Queue(ctx, i)) + } + + shutdownCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + require.NoError(t, q.Shutdown(shutdownCtx)) + + // Because multiHandler tries to read all available jobs at once, we typically + // expect them all in a single batch, but it depends on scheduling. This test + // at least ensures we see all five in total. + foundItems := 0 + for _, batch := range allBatches { + foundItems += len(batch) + } + assert.Equal(t, 5, foundItems, "expecting total of 5 items processed in batches") +} + +// Verifies that queueing a job after calling Shutdown returns ErrQueueShutdown +func TestJobQueueQueueShutdown(t *testing.T) { + h := jobqueue.JobHandler(func(ctx context.Context, j int) error { + return nil + }) + + q := jobqueue.NewJobQueue[int](h) + q.Startup() + + ctx := context.Background() + require.NoError(t, q.Queue(ctx, 42)) + + shutdownCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + require.NoError(t, q.Shutdown(shutdownCtx)) + + // After shutdown, queueing a job should fail with ErrQueueShutdown + err := q.Queue(ctx, 99) + assert.ErrorIs(t, err, jobqueue.ErrQueueShutdown) +} + +// Verifies that if the context is canceled before calling Queue, the call returns context.Canceled. +func TestJobQueueContextCancellation(t *testing.T) { + h := jobqueue.JobHandler(func(ctx context.Context, job int) error { + return nil + }) + + q := jobqueue.NewJobQueue[int](h) + q.Startup() + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // queueing after the context is canceled + err := q.Queue(ctx, 123) + require.Error(t, err) + assert.ErrorIs(t, err, context.Canceled) +} + +// Demonstrates buffering behavior. The queue is given a small concurrency level (1) but a buffer of 2. +// We enqueue 4 jobs, confirming that after the first 3 are accepted, the fourth must wait until the queue has capacity, +// and eventually, all 4 get processed +func TestJobQueueBuffer(t *testing.T) { + var mu sync.Mutex + var processed []int + + h := jobqueue.JobHandler(func(ctx context.Context, j int) error { + time.Sleep(50 * time.Millisecond) + mu.Lock() + defer mu.Unlock() + processed = append(processed, j) + return nil + }) + + q := jobqueue.NewJobQueue[int](h, + jobqueue.WithBuffer(2), + jobqueue.WithConcurrency(1), + ) + + q.Startup() + + ctx := context.Background() + // We push more jobs than concurrency. The buffer is 2, so total of 3 can be + // queued at once (one actively processed, 2 in the channel). + for i := 0; i <= 2; i++ { + require.NoError(t, q.Queue(ctx, i)) + } + + // This next queue operation should block until at least one job is processed + // or context canceled. We'll do it in a separate goroutine with a short delay. + var queueErr error + var wg sync.WaitGroup + wg.Add(1) + go func() { + queueErr = q.Queue(ctx, 99) + wg.Done() + }() + + wg.Wait() + + shutdownCtx, cancel := context.WithTimeout(ctx, 4*time.Second) + defer cancel() + assert.NoError(t, q.Shutdown(shutdownCtx), "queue shut down should be successful") + + // The queueErr must not be an error at this point; the capacity eventually + // freed up and job #99 was queued + assert.NoError(t, queueErr) + + // We expect all 4 jobs to eventually be processed + assert.Len(t, processed, 4, "all jobs should have processed") +} + +func TestJobQueueStress(t *testing.T) { + ctx := context.Background() + var mu sync.Mutex + var processed []int + + h := jobqueue.JobHandler(func(ctx context.Context, j int) error { + time.Sleep(1 * time.Millisecond) + mu.Lock() + defer mu.Unlock() + processed = append(processed, j) + return nil + }) + + q := jobqueue.NewJobQueue[int](h, + jobqueue.WithBuffer(5), + jobqueue.WithConcurrency(5), + ) + + q.Startup() + + for i := range 10_000 { + require.NoError(t, q.Queue(ctx, i)) + } + + require.NoError(t, q.Shutdown(ctx)) + + require.Equal(t, len(processed), 10_000) + for i := range 10_000 { + require.True(t, slices.Contains(processed, i)) + } +} diff --git a/internal/pdpsigner/client/client.go b/internal/pdpsigner/client/client.go new file mode 100644 index 00000000..de219c03 --- /dev/null +++ b/internal/pdpsigner/client/client.go @@ -0,0 +1,272 @@ +package client + +import ( + "bytes" + "context" + "fmt" + "math/big" + "net/http" + "net/url" + + "github.com/ethereum/go-ethereum/common" + "github.com/fil-forge/filecoin-services/go/eip712" + "github.com/fil-forge/forge/protocol/commands/pdp/sign" + "github.com/fil-forge/ucantone/client" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/ipfs/go-cid" + + "github.com/fil-forge/forge/internal/pdpsigner/types" +) + +// Client uses UCAN invocations to request a remote signing service to sign PDP operations. +type Client struct { + ServiceDID did.DID // DID of the remote signing service. + HTTP *client.HTTPClient // ucantone HTTP client. + InvocationOptions []invocation.Option // options added to every invocation. +} + +// Verify that Client implements types.SigningService at compile time +var _ types.SigningService = (*Client)(nil) + +type clientConfig struct { + httpClient *http.Client + invocationOptions []invocation.Option +} + +type Option func(*clientConfig) + +// WithHTTPClient configures a custom HTTP client. +func WithHTTPClient(c *http.Client) Option { + return func(cfg *clientConfig) { + cfg.httpClient = c + } +} + +// WithInvocationOptions configures invocation options that are sent with every +// request. +func WithInvocationOptions(opts ...invocation.Option) Option { + return func(cfg *clientConfig) { + cfg.invocationOptions = append(cfg.invocationOptions, opts...) + } +} + +// New creates a new client for the signing service. +func New(serviceDID did.DID, serviceURL string, options ...Option) (*Client, error) { + cfg := clientConfig{} + for _, opt := range options { + opt(&cfg) + } + endpoint, err := url.Parse(serviceURL) + if err != nil { + return nil, fmt.Errorf("parsing signing service URL: %w", err) + } + var httpOpts []client.HTTPOption + if cfg.httpClient != nil { + httpOpts = append(httpOpts, client.WithHTTPClient(cfg.httpClient)) + } + httpClient, err := client.NewHTTP(endpoint, httpOpts...) + if err != nil { + return nil, fmt.Errorf("creating signing service HTTP client: %w", err) + } + return &Client{ + ServiceDID: serviceDID, + HTTP: httpClient, + InvocationOptions: cfg.invocationOptions, + }, nil +} + +// SignCreateDataSet signs a CreateDataSet operation via UCAN invocation. +func (c *Client) SignCreateDataSet( + ctx context.Context, + issuer ucan.Issuer, + dataSet *big.Int, + payee common.Address, + metadata []eip712.MetadataEntry, + proofs []ucan.Delegation, + options ...invocation.Option, +) (*eip712.AuthSignature, error) { + args := &sign.DataSetCreateArguments{ + DataSet: dataSet, + Payee: payee.Bytes(), + Metadata: fromEIP712MetadataEntries(metadata), + } + inv, err := sign.DataSetCreate.Invoke(issuer, c.ServiceDID, args, c.mergeOptionsWithProofs(options, proofs)...) + if err != nil { + return nil, fmt.Errorf("invoking %s: %w", sign.DataSetCreate.Command, err) + } + return c.execAuthSignatureRequest(ctx, inv, execution.WithDelegations(proofs...)) +} + +// SignAddPieces signs an AddPieces operation via UCAN invocation. The +// caller MUST supply a proofContainer that includes every blob/accept +// invocation referenced by pieceProofs[].Proofs, plus their pdp/accept +// receipts. +func (c *Client) SignAddPieces( + ctx context.Context, + issuer ucan.Issuer, + dataSet *big.Int, + nonce *big.Int, + pieceData [][]byte, + metadata [][]eip712.MetadataEntry, + pieceProofs []sign.PieceProofs, + proofContainer ucan.Container, + proofs []ucan.Delegation, + options ...invocation.Option, +) (*eip712.AuthSignature, error) { + meta := make([]sign.Metadata, 0, len(metadata)) + for _, m := range metadata { + meta = append(meta, fromEIP712MetadataEntries(m)) + } + args := &sign.PiecesAddArguments{ + DataSet: dataSet, + Nonce: nonce, + PieceData: pieceData, + Metadata: meta, + Proofs: pieceProofs, + } + inv, err := sign.PiecesAdd.Invoke(issuer, c.ServiceDID, args, c.mergeOptionsWithProofs(options, proofs)...) + if err != nil { + return nil, fmt.Errorf("invoking %s: %w", sign.PiecesAdd.Command, err) + } + + reqOpts := []execution.RequestOption{execution.WithDelegations(proofs...)} + if proofContainer != nil { + reqOpts = append(reqOpts, + execution.WithInvocations(proofContainer.Invocations()...), + execution.WithReceipts(proofContainer.Receipts()...), + execution.WithDelegations(proofContainer.Delegations()...), + ) + } + return c.execAuthSignatureRequest(ctx, inv, reqOpts...) +} + +// SignSchedulePieceRemovals signs a SchedulePieceRemovals operation via UCAN invocation. +func (c *Client) SignSchedulePieceRemovals( + ctx context.Context, + issuer ucan.Issuer, + dataSet *big.Int, + pieceIds []*big.Int, + proofs []ucan.Delegation, + options ...invocation.Option, +) (*eip712.AuthSignature, error) { + args := &sign.PiecesRemoveScheduleArguments{ + DataSet: dataSet, + Pieces: pieceIds, + } + inv, err := sign.PiecesRemoveSchedule.Invoke(issuer, c.ServiceDID, args, c.mergeOptionsWithProofs(options, proofs)...) + if err != nil { + return nil, fmt.Errorf("invoking %s: %w", sign.PiecesRemoveSchedule.Command, err) + } + return c.execAuthSignatureRequest(ctx, inv, execution.WithDelegations(proofs...)) +} + +// SignDeleteDataSet signs a DeleteDataSet operation via UCAN invocation. +func (c *Client) SignDeleteDataSet( + ctx context.Context, + issuer ucan.Issuer, + dataSet *big.Int, + proofs []ucan.Delegation, + options ...invocation.Option, +) (*eip712.AuthSignature, error) { + args := &sign.DataSetDeleteArguments{DataSet: dataSet} + inv, err := sign.DataSetDelete.Invoke(issuer, c.ServiceDID, args, c.mergeOptionsWithProofs(options, proofs)...) + if err != nil { + return nil, fmt.Errorf("invoking %s: %w", sign.DataSetDelete.Command, err) + } + return c.execAuthSignatureRequest(ctx, inv, execution.WithDelegations(proofs...)) +} + +func (c *Client) mergeOptionsWithProofs(options []invocation.Option, proofs []ucan.Delegation) []invocation.Option { + all := make([]invocation.Option, 0, len(c.InvocationOptions)+len(options)+1) + all = append(all, c.InvocationOptions...) + all = append(all, options...) + if len(proofs) > 0 { + links := make([]cid.Cid, len(proofs)) + for i, p := range proofs { + links[i] = p.Link() + } + all = append(all, invocation.WithProofs(links...)) + } + return all +} + +func (c *Client) execAuthSignatureRequest(ctx context.Context, inv ucan.Invocation, reqOpts ...execution.RequestOption) (*eip712.AuthSignature, error) { + req := execution.NewRequest(ctx, inv, reqOpts...) + resp, err := c.HTTP.Execute(req) + if err != nil { + return nil, fmt.Errorf("executing %s: %w", inv.Command(), err) + } + + rcpt := resp.Receipt() + out := rcpt.Out() + okBytes, errBytes := out.Unpack() + if !out.IsOK() { + // Try to decode as a named error; fall back to a generic message. + named := errors.Named(nil) + if e := tryDecodeNamedError(errBytes); e != nil { + named = e + } + if named != nil { + return nil, named + } + return nil, fmt.Errorf("%s failed: %s", inv.Command(), string(errBytes)) + } + + var sig sign.AuthSignature + if err := sig.UnmarshalCBOR(bytes.NewReader(okBytes)); err != nil { + return nil, fmt.Errorf("decoding signature receipt: %w", err) + } + out712, err := fromLibforgeAuthSig(sig) + if err != nil { + return nil, fmt.Errorf("converting signature: %w", err) + } + return out712, nil +} + +// tryDecodeNamedError attempts to decode raw failure bytes as the ucantone +// error datamodel; returns nil if the bytes don't match. +func tryDecodeNamedError(b []byte) errors.Named { + // errors.New returns a value implementing the Named interface and + // MarshalCBOR-able as a tagged error model. For now we don't decode it + // back into a typed value — callers get the raw bytes via the fallback. + // Hook left in place for future structured failure parsing. + _ = b + return nil +} + +func fromEIP712MetadataEntries(entries []eip712.MetadataEntry) sign.Metadata { + meta := sign.Metadata{ + Keys: make([]string, 0, len(entries)), + Values: make(map[string]string, len(entries)), + } + for _, e := range entries { + meta.Keys = append(meta.Keys, e.Key) + meta.Values[e.Key] = e.Value + } + return meta +} + +func fromLibforgeAuthSig(s sign.AuthSignature) (*eip712.AuthSignature, error) { + if len(s.R) != len(common.Hash{}) { + return nil, fmt.Errorf("invalid R length: got %d, want %d", len(s.R), len(common.Hash{})) + } + if len(s.S) != len(common.Hash{}) { + return nil, fmt.Errorf("invalid S length: got %d, want %d", len(s.S), len(common.Hash{})) + } + if len(s.Signer) != len(common.Address{}) { + return nil, fmt.Errorf("invalid Signer length: got %d, want %d", len(s.Signer), len(common.Address{})) + } + out := &eip712.AuthSignature{ + Signature: s.Signature, + V: s.V, + SignedData: s.SignedData, + } + copy(out.R[:], s.R) + copy(out.S[:], s.S) + out.Signer.SetBytes(s.Signer) + return out, nil +} diff --git a/internal/pdpsigner/client/client_test.go b/internal/pdpsigner/client/client_test.go new file mode 100644 index 00000000..8be9a639 --- /dev/null +++ b/internal/pdpsigner/client/client_test.go @@ -0,0 +1,209 @@ +package client + +import ( + "math/big" + "net/http" + "net/url" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/fil-forge/filecoin-services/go/eip712" + "github.com/fil-forge/forge/protocol/commands/pdp/sign" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/client" + uerrors "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/testutil" + "github.com/fil-forge/ucantone/ucan" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mustDelegate issues a delegation from `issuer` to `audience.DID()` for +// `cap`, with the issuer's own DID as the subject. Bails the test if +// delegation fails. +func mustDelegate[Args, OK binding.CBORValue]( + t *testing.T, + cap binding.Binding[Args, OK], + issuer ucan.Issuer, + audience ucan.Principal, +) ucan.Delegation { + t.Helper() + dlg, err := cap.Delegate(issuer, audience.DID(), issuer.DID()) + require.NoError(t, err) + return dlg +} + +// newClientBackedByServer wires a ucantone HTTPClient to use the supplied +// HTTPServer as its transport. That way invocations go through the full +// encode → decode → handle → encode response → decode cycle without +// listening on a real port. +func newClientBackedByServer(t *testing.T, srv *server.HTTPServer, service ucan.Issuer) *Client { + t.Helper() + endpoint, err := url.Parse("http://test") + require.NoError(t, err) + httpClient, err := client.NewHTTP(endpoint, client.WithHTTPClient(&http.Client{Transport: srv})) + require.NoError(t, err) + return &Client{ServiceDID: service.DID(), HTTP: httpClient} +} + +func TestClient_SignCreateDataSet(t *testing.T) { + mock := mockLibforgeSignature() + service := testutil.RandomIssuer(t) + alice := testutil.RandomIssuer(t) + + srv := server.NewHTTP(service) + srv.Handle(sign.DataSetCreate.Command, binding.NewHandler( + func(req *binding.Request[*sign.DataSetCreateArguments], res *binding.Response[*sign.DataSetCreateOK]) error { + args := req.Task().Arguments() + assert.Equal(t, "12345", args.DataSet.String()) + assert.Equal(t, "0xabCDEF1234567890ABcDEF1234567890aBCDeF12", common.BytesToAddress(args.Payee).String()) + assert.Len(t, args.Metadata.Keys, 1) + assert.Equal(t, "test-key", args.Metadata.Keys[0]) + return res.SetSuccess(&mock) + }, + )) + + c := newClientBackedByServer(t, srv, service) + dlg := mustDelegate(t, sign.DataSetCreate, service, alice) + + signature, err := c.SignCreateDataSet( + t.Context(), + alice, + big.NewInt(12345), + common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12"), + []eip712.MetadataEntry{{Key: "test-key", Value: "test-value"}}, + []ucan.Delegation{dlg}, + ) + require.NoError(t, err) + require.NotNil(t, signature) + assert.Equal(t, common.BytesToAddress(mock.Signer), signature.Signer) +} + +func TestClient_SignAddPieces(t *testing.T) { + mock := mockLibforgeSignature() + service := testutil.RandomIssuer(t) + alice := testutil.RandomIssuer(t) + + srv := server.NewHTTP(service) + srv.Handle(sign.PiecesAdd.Command, binding.NewHandler( + func(req *binding.Request[*sign.PiecesAddArguments], res *binding.Response[*sign.PiecesAddOK]) error { + args := req.Task().Arguments() + assert.Equal(t, "12345", args.DataSet.String()) + assert.Equal(t, "0", args.Nonce.String()) + assert.Len(t, args.PieceData, 2) + assert.Equal(t, []byte("piece1"), args.PieceData[0]) + return res.SetSuccess(&mock) + }, + )) + + c := newClientBackedByServer(t, srv, service) + dlg := mustDelegate(t, sign.PiecesAdd, service, alice) + + signature, err := c.SignAddPieces( + t.Context(), + alice, + big.NewInt(12345), + big.NewInt(0), + [][]byte{[]byte("piece1"), []byte("piece2")}, + [][]eip712.MetadataEntry{ + {{Key: "piece1-key", Value: "piece1-value"}}, + {{Key: "piece2-key", Value: "piece2-value"}}, + }, + nil, // pieceProofs + nil, // proofContainer + []ucan.Delegation{dlg}, + ) + require.NoError(t, err) + require.NotNil(t, signature) + assert.Equal(t, common.BytesToAddress(mock.Signer), signature.Signer) +} + +func TestClient_SignSchedulePieceRemovals(t *testing.T) { + mock := mockLibforgeSignature() + service := testutil.RandomIssuer(t) + alice := testutil.RandomIssuer(t) + + srv := server.NewHTTP(service) + srv.Handle(sign.PiecesRemoveSchedule.Command, binding.NewHandler( + func(req *binding.Request[*sign.PiecesRemoveScheduleArguments], res *binding.Response[*sign.PiecesRemoveScheduleOK]) error { + args := req.Task().Arguments() + assert.Equal(t, "12345", args.DataSet.String()) + assert.Len(t, args.Pieces, 3) + return res.SetSuccess(&mock) + }, + )) + + c := newClientBackedByServer(t, srv, service) + dlg := mustDelegate(t, sign.PiecesRemoveSchedule, service, alice) + + signature, err := c.SignSchedulePieceRemovals( + t.Context(), + alice, + big.NewInt(12345), + []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)}, + []ucan.Delegation{dlg}, + ) + require.NoError(t, err) + require.NotNil(t, signature) + assert.Equal(t, common.BytesToAddress(mock.Signer), signature.Signer) +} + +func TestClient_SignDeleteDataSet(t *testing.T) { + mock := mockLibforgeSignature() + service := testutil.RandomIssuer(t) + alice := testutil.RandomIssuer(t) + + srv := server.NewHTTP(service) + srv.Handle(sign.DataSetDelete.Command, binding.NewHandler( + func(req *binding.Request[*sign.DataSetDeleteArguments], res *binding.Response[*sign.DataSetDeleteOK]) error { + args := req.Task().Arguments() + assert.Equal(t, "12345", args.DataSet.String()) + return res.SetSuccess(&mock) + }, + )) + + c := newClientBackedByServer(t, srv, service) + dlg := mustDelegate(t, sign.DataSetDelete, service, alice) + + signature, err := c.SignDeleteDataSet(t.Context(), alice, big.NewInt(12345), []ucan.Delegation{dlg}) + require.NoError(t, err) + require.NotNil(t, signature) + assert.Equal(t, common.BytesToAddress(mock.Signer), signature.Signer) +} + +func TestClient_ServerError(t *testing.T) { + service := testutil.RandomIssuer(t) + alice := testutil.RandomIssuer(t) + + srv := server.NewHTTP(service) + srv.Handle(sign.DataSetCreate.Command, binding.NewHandler( + func(req *binding.Request[*sign.DataSetCreateArguments], res *binding.Response[*sign.DataSetCreateOK]) error { + return res.SetFailure(uerrors.New("Internal", "boom")) + }, + )) + + c := newClientBackedByServer(t, srv, service) + dlg := mustDelegate(t, sign.DataSetCreate, service, alice) + + _, err := c.SignCreateDataSet( + t.Context(), + alice, + big.NewInt(12345), + common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12"), + []eip712.MetadataEntry{}, + []ucan.Delegation{dlg}, + ) + require.Error(t, err) +} + +func mockLibforgeSignature() sign.AuthSignature { + return sign.AuthSignature{ + Signature: []byte{0x01, 0x02, 0x03}, + V: 27, + R: common.BigToHash(big.NewInt(12345)).Bytes(), + S: common.BigToHash(big.NewInt(67890)).Bytes(), + SignedData: []byte{0xaa, 0xbb}, + Signer: common.HexToAddress("0x1234567890123456789012345678901234567890").Bytes(), + } +} diff --git a/internal/pdpsigner/inprocess/signer.go b/internal/pdpsigner/inprocess/signer.go new file mode 100644 index 00000000..10ed8416 --- /dev/null +++ b/internal/pdpsigner/inprocess/signer.go @@ -0,0 +1,74 @@ +package inprocess + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/fil-forge/filecoin-services/go/eip712" + "github.com/fil-forge/forge/protocol/commands/pdp/sign" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" + + "github.com/fil-forge/forge/internal/pdpsigner/signer" + "github.com/fil-forge/forge/internal/pdpsigner/types" +) + +// Signer implements [types.SigningService] using eip712.Signer directly. +// This provides an in-process implementation that bypasses network calls and +// authorization checks — useful for testing and development. +type Signer struct { + signer *signer.Signer +} + +// Verify that Signer implements types.SigningService at compile time +var _ types.SigningService = (*Signer)(nil) + +// New creates a new in-process signing service +func New(signer *signer.Signer) *Signer { + return &Signer{signer: signer} +} + +// SignCreateDataSet signs a CreateDataSet operation directly. +func (s *Signer) SignCreateDataSet(_ context.Context, + _ ucan.Issuer, + clientDataSetId *big.Int, + payee common.Address, + metadata []eip712.MetadataEntry, + _ []ucan.Delegation, + _ ...invocation.Option) (*eip712.AuthSignature, error) { + return s.signer.SignCreateDataSet(clientDataSetId, payee, metadata) +} + +// SignAddPieces signs an AddPieces operation directly. +func (s *Signer) SignAddPieces(_ context.Context, + _ ucan.Issuer, + clientDataSetId *big.Int, + nonce *big.Int, + pieceData [][]byte, + metadata [][]eip712.MetadataEntry, + _ []sign.PieceProofs, + _ ucan.Container, + _ []ucan.Delegation, + _ ...invocation.Option) (*eip712.AuthSignature, error) { + return s.signer.SignAddPieces(clientDataSetId, nonce, pieceData, metadata) +} + +// SignSchedulePieceRemovals signs a SchedulePieceRemovals operation directly. +func (s *Signer) SignSchedulePieceRemovals(_ context.Context, + _ ucan.Issuer, + clientDataSetId *big.Int, + pieceIds []*big.Int, + _ []ucan.Delegation, + _ ...invocation.Option) (*eip712.AuthSignature, error) { + return s.signer.SignSchedulePieceRemovals(clientDataSetId, pieceIds) +} + +// SignDeleteDataSet signs a DeleteDataSet operation directly. +func (s *Signer) SignDeleteDataSet(_ context.Context, + _ ucan.Issuer, + clientDataSetId *big.Int, + _ []ucan.Delegation, + _ ...invocation.Option) (*eip712.AuthSignature, error) { + return s.signer.SignDeleteDataSet(clientDataSetId) +} diff --git a/internal/pdpsigner/inprocess/signer_test.go b/internal/pdpsigner/inprocess/signer_test.go new file mode 100644 index 00000000..b69e262f --- /dev/null +++ b/internal/pdpsigner/inprocess/signer_test.go @@ -0,0 +1,160 @@ +package inprocess + +import ( + "context" + "crypto/ecdsa" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/fil-forge/filecoin-services/go/eip712" + "github.com/fil-forge/ucantone/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/fil-forge/forge/internal/pdpsigner/signer" + "github.com/fil-forge/forge/internal/pdpsigner/types" +) + +func setupTestSigner(t *testing.T) (*Signer, *ecdsa.PrivateKey) { + // Generate a test private key + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + + // Create a test contract address + contractAddr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // Create a test chain ID + chainID := big.NewInt(31415926) + + // Create the signer + eip712Signer := signer.NewSigner(privateKey, chainID, contractAddr) + + // Create the in-process signer + signer := New(eip712Signer) + + return signer, privateKey +} + +func TestSigner_ImplementsInterface(t *testing.T) { + signer, _ := setupTestSigner(t) + var _ types.SigningService = signer +} + +func TestSigner_SignCreateDataSet(t *testing.T) { + s, privateKey := setupTestSigner(t) + issuer := testutil.RandomIssuer(t) + ctx := context.Background() + + clientDataSetId := big.NewInt(12345) + payee := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + metadata := []eip712.MetadataEntry{ + {Key: "test-key", Value: "test-value"}, + } + + signature, err := s.SignCreateDataSet(ctx, issuer, clientDataSetId, payee, metadata, nil) + require.NoError(t, err) + assert.NotNil(t, signature) + + expectedAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + assert.Equal(t, expectedAddress, signature.Signer) + + assert.NotEmpty(t, signature.R) + assert.NotEmpty(t, signature.S) + assert.NotZero(t, signature.V) +} + +func TestSigner_SignAddPieces(t *testing.T) { + s, privateKey := setupTestSigner(t) + issuer := testutil.RandomIssuer(t) + ctx := context.Background() + + clientDataSetId := big.NewInt(12345) + firstAdded := big.NewInt(0) + pieceData := [][]byte{ + []byte("piece1"), + []byte("piece2"), + } + metadata := [][]eip712.MetadataEntry{ + {{Key: "piece1-key", Value: "piece1-value"}}, + {{Key: "piece2-key", Value: "piece2-value"}}, + } + + signature, err := s.SignAddPieces(ctx, issuer, clientDataSetId, firstAdded, pieceData, metadata, nil, nil, nil) + require.NoError(t, err) + assert.NotNil(t, signature) + + expectedAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + assert.Equal(t, expectedAddress, signature.Signer) + + assert.NotEmpty(t, signature.R) + assert.NotEmpty(t, signature.S) + assert.NotZero(t, signature.V) +} + +func TestSigner_SignSchedulePieceRemovals(t *testing.T) { + s, privateKey := setupTestSigner(t) + issuer := testutil.RandomIssuer(t) + ctx := context.Background() + + clientDataSetId := big.NewInt(12345) + pieceIds := []*big.Int{ + big.NewInt(1), + big.NewInt(2), + big.NewInt(3), + } + + signature, err := s.SignSchedulePieceRemovals(ctx, issuer, clientDataSetId, pieceIds, nil) + require.NoError(t, err) + assert.NotNil(t, signature) + + expectedAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + assert.Equal(t, expectedAddress, signature.Signer) + + assert.NotEmpty(t, signature.R) + assert.NotEmpty(t, signature.S) + assert.NotZero(t, signature.V) +} + +func TestSigner_SignDeleteDataSet(t *testing.T) { + s, privateKey := setupTestSigner(t) + issuer := testutil.RandomIssuer(t) + ctx := context.Background() + + clientDataSetId := big.NewInt(12345) + + signature, err := s.SignDeleteDataSet(ctx, issuer, clientDataSetId, nil) + require.NoError(t, err) + assert.NotNil(t, signature) + + expectedAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + assert.Equal(t, expectedAddress, signature.Signer) + + assert.NotEmpty(t, signature.R) + assert.NotEmpty(t, signature.S) + assert.NotZero(t, signature.V) +} + +func TestSigner_SignatureConsistency(t *testing.T) { + s, _ := setupTestSigner(t) + issuer := testutil.RandomIssuer(t) + ctx := context.Background() + + clientDataSetId := big.NewInt(12345) + payee := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12") + metadata := []eip712.MetadataEntry{ + {Key: "test-key", Value: "test-value"}, + } + + sig1, err := s.SignCreateDataSet(ctx, issuer, clientDataSetId, payee, metadata, nil) + require.NoError(t, err) + + sig2, err := s.SignCreateDataSet(ctx, issuer, clientDataSetId, payee, metadata, nil) + require.NoError(t, err) + + assert.Equal(t, sig1.R, sig2.R) + assert.Equal(t, sig1.S, sig2.S) + assert.Equal(t, sig1.V, sig2.V) + assert.Equal(t, sig1.Signer, sig2.Signer) +} diff --git a/internal/pdpsigner/signer/signer.go b/internal/pdpsigner/signer/signer.go new file mode 100644 index 00000000..2b695876 --- /dev/null +++ b/internal/pdpsigner/signer/signer.go @@ -0,0 +1,193 @@ +package signer + +import ( + "crypto/ecdsa" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/fil-forge/filecoin-services/go/eip712" +) + +type Signer struct { + privateKey *ecdsa.PrivateKey + address common.Address + chainId *big.Int + verifyingContract common.Address +} + +func NewSigner(privateKey *ecdsa.PrivateKey, chainId *big.Int, verifyingContract common.Address) *Signer { + address := crypto.PubkeyToAddress(privateKey.PublicKey) + return &Signer{ + privateKey: privateKey, + address: address, + chainId: chainId, + verifyingContract: verifyingContract, + } +} + +func (s *Signer) GetAddress() common.Address { + return s.address +} + +func (s *Signer) SignCreateDataSet(clientDataSetId *big.Int, payee common.Address, metadata []eip712.MetadataEntry) (*eip712.AuthSignature, error) { + // Convert metadata to the format expected by apitypes + metadataArray := make([]map[string]interface{}, len(metadata)) + for i, entry := range metadata { + metadataArray[i] = map[string]interface{}{ + "key": entry.Key, + "value": entry.Value, + } + } + + message := map[string]interface{}{ + "clientDataSetId": clientDataSetId, + "payee": strings.ToLower(payee.Hex()), + "metadata": metadataArray, + } + + return s.signTypedData("CreateDataSet", message) +} + +// RecoverCreateDataSetSigner recovers the signer address from a CreateDataSet signature +func (s *Signer) RecoverCreateDataSetSigner(clientDataSetId *big.Int, payee common.Address, metadata []eip712.MetadataEntry, signature *eip712.AuthSignature) (common.Address, error) { + // Convert metadata to the format expected by apitypes + metadataArray := make([]map[string]interface{}, len(metadata)) + for i, entry := range metadata { + metadataArray[i] = map[string]interface{}{ + "key": entry.Key, + "value": entry.Value, + } + } + + message := map[string]interface{}{ + "clientDataSetId": clientDataSetId, + "payee": strings.ToLower(payee.Hex()), + "metadata": metadataArray, + } + + domain := eip712.GetDomain(s.chainId, s.verifyingContract) + hash, err := eip712.GetMessageHash(domain, "CreateDataSet", message) + if err != nil { + return common.Address{}, fmt.Errorf("failed to get message hash: %w", err) + } + + // crypto.SigToPub expects v to be 0 or 1, but signature has v as 27 or 28 + // Create a copy with adjusted v for recovery + recoverySignature := make([]byte, 65) + copy(recoverySignature, signature.Signature) + if recoverySignature[64] >= 27 { + recoverySignature[64] -= 27 + } + + // Recover the public key from the signature + pubKey, err := crypto.SigToPub(hash, recoverySignature) + if err != nil { + return common.Address{}, fmt.Errorf("failed to recover public key: %w", err) + } + + return crypto.PubkeyToAddress(*pubKey), nil +} + +func (s *Signer) SignAddPieces(clientDataSetId, nonce *big.Int, pieceData [][]byte, metadata [][]eip712.MetadataEntry) (*eip712.AuthSignature, error) { + cids := make([]map[string]interface{}, len(pieceData)) + for i, data := range pieceData { + cids[i] = map[string]interface{}{ + "data": data, + } + } + + pieceMetadata := make([]map[string]interface{}, len(metadata)) + for i, meta := range metadata { + // Convert MetadataEntry array to expected format + metadataArray := make([]map[string]interface{}, len(meta)) + for j, entry := range meta { + metadataArray[j] = map[string]interface{}{ + "key": entry.Key, + "value": entry.Value, + } + } + + pieceMetadata[i] = map[string]interface{}{ + "pieceIndex": big.NewInt(int64(i)), + "metadata": metadataArray, + } + } + + message := map[string]interface{}{ + "clientDataSetId": clientDataSetId, + "nonce": nonce, + "pieceData": cids, + "pieceMetadata": pieceMetadata, + } + + return s.signTypedData("AddPieces", message) +} + +func (s *Signer) SignSchedulePieceRemovals(clientDataSetId *big.Int, pieceIds []*big.Int) (*eip712.AuthSignature, error) { + message := map[string]interface{}{ + "clientDataSetId": clientDataSetId, + "pieceIds": pieceIds, + } + + return s.signTypedData("SchedulePieceRemovals", message) +} + +func (s *Signer) SignDeleteDataSet(clientDataSetId *big.Int) (*eip712.AuthSignature, error) { + message := map[string]interface{}{ + "clientDataSetId": clientDataSetId, + } + + return s.signTypedData("DeleteDataSet", message) +} + +func (s *Signer) signTypedData(primaryType string, message map[string]interface{}) (*eip712.AuthSignature, error) { + domain := eip712.GetDomain(s.chainId, s.verifyingContract) + + // Get the EIP-712 hash to sign + hash, err := eip712.GetMessageHash(domain, primaryType, message) + if err != nil { + return nil, fmt.Errorf("failed to get message hash: %w", err) + } + + // Sign the hash + signature, err := crypto.Sign(hash, s.privateKey) + if err != nil { + return nil, fmt.Errorf("failed to sign: %w", err) + } + + // Verify we can recover the correct signer + pubKey, err := crypto.SigToPub(hash, signature) + if err != nil { + return nil, fmt.Errorf("failed to recover public key: %w", err) + } + recoveredAddr := crypto.PubkeyToAddress(*pubKey) + if recoveredAddr != s.address { + return nil, fmt.Errorf("signature verification failed: expected %s, recovered %s", s.address.Hex(), recoveredAddr.Hex()) + } + + // Transform V from recovery ID to Ethereum signature standard + // Ethereum uses 27/28, crypto.Sign returns 0/1 + v := signature[64] + 27 + + // Extract r and s + r := common.BytesToHash(signature[:32]) + sig_s := common.BytesToHash(signature[32:64]) + + // Create full signature with adjusted V + fullSig := make([]byte, 65) + copy(fullSig[:32], signature[:32]) + copy(fullSig[32:64], signature[32:64]) + fullSig[64] = v + + return &eip712.AuthSignature{ + Signature: fullSig, + V: v, + R: r, + S: sig_s, + SignedData: hash, + Signer: s.address, + }, nil +} diff --git a/internal/pdpsigner/types/types.go b/internal/pdpsigner/types/types.go new file mode 100644 index 00000000..1c149811 --- /dev/null +++ b/internal/pdpsigner/types/types.go @@ -0,0 +1,117 @@ +package types + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/fil-forge/filecoin-services/go/eip712" + "github.com/fil-forge/forge/protocol/commands/pdp/sign" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/invocation" +) + +// CreateDataSetRequest represents the request payload for creating a dataset +type CreateDataSetRequest struct { + ClientDataSetId string `json:"clientDataSetId"` + Payee string `json:"payee"` + Metadata []eip712.MetadataEntry `json:"metadata"` +} + +// AddPiecesRequest represents the request payload for adding pieces +type AddPiecesRequest struct { + ClientDataSetId string `json:"clientDataSetId"` + Nonce string `json:"nonce"` + PieceData []string `json:"pieceData"` // hex-encoded bytes + Metadata [][]eip712.MetadataEntry `json:"metadata"` +} + +// SchedulePieceRemovalsRequest represents the request payload for scheduling piece removals +type SchedulePieceRemovalsRequest struct { + ClientDataSetId string `json:"clientDataSetId"` + PieceIds []string `json:"pieceIds"` +} + +// DeleteDataSetRequest represents the request payload for deleting a dataset +type DeleteDataSetRequest struct { + ClientDataSetId string `json:"clientDataSetId"` +} + +// HealthResponse represents the health check response +type HealthResponse struct { + Status string `json:"status"` + Signer string `json:"signer"` +} + +// SigningService defines the interface for authorized PDP operation signing. +// This can be implemented by: +// - UCAN client (remote signing service) +// - In-process signer (for testing/dev) +// +// The proofs parameter on each method threads UCAN delegations through: +// their CIDs are attached as invocation proofs and the envelopes ride in +// the execution request for server-side authorization. Pass an empty +// slice when authorization is handled out-of-band (e.g. in-process). +type SigningService interface { + // SignCreateDataSet signs a CreateDataSet operation + SignCreateDataSet( + ctx context.Context, + issuer ucan.Issuer, + dataSet *big.Int, + payee common.Address, + metadata []eip712.MetadataEntry, + proofs []ucan.Delegation, + options ...invocation.Option, + ) (*eip712.AuthSignature, error) + + // SignAddPieces signs an AddPieces operation. The pieceProofs field + // carries the per-piece blob/accept invocation CIDs that prove + // sub-pieces; the referenced invocations and their pdp/accept receipts + // MUST be present in proofContainer. + SignAddPieces( + ctx context.Context, + issuer ucan.Issuer, + dataSet *big.Int, + nonce *big.Int, + pieceData [][]byte, + metadata [][]eip712.MetadataEntry, + pieceProofs []sign.PieceProofs, + proofContainer ucan.Container, + proofs []ucan.Delegation, + options ...invocation.Option, + ) (*eip712.AuthSignature, error) + + // SignSchedulePieceRemovals signs a SchedulePieceRemovals operation + SignSchedulePieceRemovals( + ctx context.Context, + issuer ucan.Issuer, + dataSet *big.Int, + pieceIds []*big.Int, + proofs []ucan.Delegation, + options ...invocation.Option, + ) (*eip712.AuthSignature, error) + + // SignDeleteDataSet signs a DeleteDataSet operation + SignDeleteDataSet( + ctx context.Context, + issuer ucan.Issuer, + dataSet *big.Int, + proofs []ucan.Delegation, + options ...invocation.Option, + ) (*eip712.AuthSignature, error) +} + +// OperationSigner defines the interface for PDP operation signing. +type OperationSigner interface { + // SignCreateDataSet signs a CreateDataSet operation + SignCreateDataSet(dataSet *big.Int, payee common.Address, metadata []eip712.MetadataEntry) (*eip712.AuthSignature, error) + + // SignAddPieces signs an AddPieces operation + SignAddPieces(dataSet *big.Int, nonce *big.Int, pieceData [][]byte, metadata [][]eip712.MetadataEntry) (*eip712.AuthSignature, error) + + // SignSchedulePieceRemovals signs a SchedulePieceRemovals operation + SignSchedulePieceRemovals(dataSet *big.Int, pieceIds []*big.Int) (*eip712.AuthSignature, error) + + // SignDeleteDataSet signs a DeleteDataSet operation + SignDeleteDataSet(dataSet *big.Int) (*eip712.AuthSignature, error) +} diff --git a/internal/piece/piece.go b/internal/piece/piece.go new file mode 100644 index 00000000..fed7e064 --- /dev/null +++ b/internal/piece/piece.go @@ -0,0 +1,118 @@ +package piece + +import ( + "fmt" + + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" + "github.com/multiformats/go-varint" +) + +// fr32Code is the multihash code for FR32_SHA256_TRUNC254_PADDED_BINARY_TREE +// (FRC-0069). Aliased for use in this package. +const fr32Code = uint64(multicodec.Fr32Sha256Trunc254Padbintree) + +// Piece is a Filecoin commP piece reference. Its on-wire form is a +// CIDv1 (Raw codec) wrapping an FR32 multihash whose payload is: +// +// varint(padding) || height (1 byte) || commitment (32 bytes) +type Piece struct { + padding uint64 + height uint8 + commitment [32]byte +} + +func (p Piece) Padding() uint64 { return p.padding } +func (p Piece) Height() uint8 { return p.height } + +func (p Piece) DataCommitment() []byte { + out := make([]byte, 32) + copy(out, p.commitment[:]) + return out +} + +// PaddedSize is the FR32-padded tree size in bytes (32 << height). +func (p Piece) PaddedSize() uint64 { return HeightToPaddedSize(p.height) } + +// Multihash builds the FR32 multihash bytes for this piece. +func (p Piece) Multihash() multihash.Multihash { + paddingSize := varint.UvarintSize(p.padding) + digestSize := 32 + 1 + paddingSize + + buf := make( + []byte, + varint.UvarintSize(fr32Code)+varint.UvarintSize(uint64(digestSize))+digestSize, + ) + pos := varint.PutUvarint(buf, fr32Code) + pos += varint.PutUvarint(buf[pos:], uint64(digestSize)) + pos += varint.PutUvarint(buf[pos:], p.padding) + buf[pos] = p.height + pos++ + copy(buf[pos:], p.commitment[:]) + return buf +} + +// CID returns the v2 piece CID: CIDv1 with Raw codec wrapping the FR32 +// multihash. Bit-identical to libstoracha's PieceLink.Link(). +func (p Piece) CID() cid.Cid { return MultihashToCommpCID(p.Multihash()) } + +// FromCID parses a v2 piece CID into a Piece. The CID must use the Raw +// codec and an FR32 multihash. +func FromCID(c cid.Cid) (Piece, error) { + if c.Prefix().Codec != uint64(multicodec.Raw) { + return Piece{}, fmt.Errorf("piece CID must use raw codec, got 0x%x", c.Prefix().Codec) + } + return FromMultihash(c.Hash()) +} + +// FromMultihash parses an FR32 multihash into a Piece. +func FromMultihash(mh multihash.Multihash) (Piece, error) { + decoded, err := multihash.Decode(mh) + if err != nil { + return Piece{}, fmt.Errorf("decoding multihash: %w", err) + } + if decoded.Code != fr32Code { + return Piece{}, fmt.Errorf("multihash code must be 0x%x, got 0x%x", fr32Code, decoded.Code) + } + + padding, n, err := varint.FromUvarint(decoded.Digest) + if err != nil { + return Piece{}, fmt.Errorf("reading padding varint: %w", err) + } + if len(decoded.Digest) < n+1+32 { + return Piece{}, fmt.Errorf("digest too short: %d bytes", len(decoded.Digest)) + } + height := decoded.Digest[n] + var commitment [32]byte + copy(commitment[:], decoded.Digest[n+1:n+1+32]) + + return Piece{padding: padding, height: height, commitment: commitment}, nil +} + +// FromCommitmentAndSize builds a Piece from a raw 32-byte data +// commitment and an unpadded input size in bytes. +func FromCommitmentAndSize(commD []byte, unpaddedDataSize uint64) (Piece, error) { + if len(commD) != 32 { + return Piece{}, fmt.Errorf("commitments must be 32 bytes long, got %d", len(commD)) + } + if unpaddedDataSize < 127 { + return Piece{}, fmt.Errorf("unpadded data size must be at least 127, got %d", unpaddedDataSize) + } + + height, padding, err := UnpaddedSizeToV1TreeHeightAndPadding(unpaddedDataSize) + if err != nil { + return Piece{}, err + } + if padding > varint.MaxValueUvarint63 { + return Piece{}, fmt.Errorf("padding must be less than 2^63-1, got %d", padding) + } + + var commitment [32]byte + copy(commitment[:], commD) + return Piece{padding: padding, height: height, commitment: commitment}, nil +} + +func MultihashToCommpCID(mh multihash.Multihash) cid.Cid { + return cid.NewCidV1(cid.Raw, mh) +} diff --git a/internal/piece/piece_test.go b/internal/piece/piece_test.go new file mode 100644 index 00000000..6930dfe2 --- /dev/null +++ b/internal/piece/piece_test.go @@ -0,0 +1,111 @@ +package piece_test + +import ( + "bytes" + "io" + "math/rand" + "testing" + + "github.com/fil-forge/forge/internal/piece" + commcid "github.com/filecoin-project/go-fil-commcid" + commp "github.com/filecoin-project/go-fil-commp-hashhash" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multicodec" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +// fixedCommP builds a deterministic 10KiB commP for use as a stable +// fixture. Returns the data commitment + the unpadded input size. +func fixedCommP(t *testing.T, seed int64, size int) (commD []byte, unpadded uint64) { + t.Helper() + r := rand.New(rand.NewSource(seed)) + data := make([]byte, size) + _, err := io.ReadFull(r, data) + require.NoError(t, err) + + c := &commp.Calc{} + _, err = io.Copy(c, bytes.NewReader(data)) + require.NoError(t, err) + commD, _, err = c.Digest() + require.NoError(t, err) + return commD, uint64(size) +} + +func TestPieceRoundTrip(t *testing.T) { + commD, unpadded := fixedCommP(t, 1, 10*1024) + + p, err := piece.FromCommitmentAndSize(commD, unpadded) + require.NoError(t, err) + require.Equal(t, commD, p.DataCommitment()) + + c := p.CID() + require.Equal(t, uint64(multicodec.Raw), c.Prefix().Codec) + require.Equal(t, uint64(multicodec.Fr32Sha256Trunc254Padbintree), c.Prefix().MhType) + + got, err := piece.FromCID(c) + require.NoError(t, err) + require.Equal(t, p.Padding(), got.Padding()) + require.Equal(t, p.Height(), got.Height()) + require.Equal(t, p.PaddedSize(), got.PaddedSize()) + require.Equal(t, p.DataCommitment(), got.DataCommitment()) +} + +// TestPieceWireCompat verifies the piri-internal Piece produces a CID +// byte-identical to the canonical go-fil-commcid implementation. This +// is the wire-compat guarantee: any existing v2 piece CID written by +// libstoracha (or by another commcid-based actor) decodes the same +// way through our type. +func TestPieceWireCompat(t *testing.T) { + cases := []struct { + name string + seed int64 + size int + }{ + {"10KiB", 1, 10 * 1024}, + {"127B", 2, 127}, + {"1MiB", 3, 1 << 20}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + commD, unpadded := fixedCommP(t, tc.seed, tc.size) + + oracleCID, err := commcid.DataCommitmentToPieceCidv2(commD, unpadded) + require.NoError(t, err) + + p, err := piece.FromCommitmentAndSize(commD, unpadded) + require.NoError(t, err) + require.Equal(t, oracleCID.String(), p.CID().String(), "wire format diverges from go-fil-commcid") + require.Equal(t, oracleCID.Bytes(), p.CID().Bytes()) + require.Equal(t, []byte(oracleCID.Hash()), []byte(p.Multihash())) + }) + } +} + +func TestFromCIDRejectsWrongCodec(t *testing.T) { + commD, unpadded := fixedCommP(t, 4, 10*1024) + p, err := piece.FromCommitmentAndSize(commD, unpadded) + require.NoError(t, err) + + bad := cid.NewCidV1(uint64(multicodec.DagCbor), p.Multihash()) + _, err = piece.FromCID(bad) + require.Error(t, err) +} + +func TestFromMultihashRejectsWrongCode(t *testing.T) { + mh, err := multihash.Sum([]byte("not a piece"), multihash.SHA2_256, -1) + require.NoError(t, err) + _, err = piece.FromMultihash(mh) + require.Error(t, err) +} + +func TestSizeHelpers(t *testing.T) { + // Fr32PaddedSizeToV1TreeHeight: sanity boundaries. + require.Equal(t, uint8(0), piece.Fr32PaddedSizeToV1TreeHeight(32)) + require.Equal(t, uint8(1), piece.Fr32PaddedSizeToV1TreeHeight(64)) + require.Equal(t, uint8(2), piece.Fr32PaddedSizeToV1TreeHeight(128)) + require.Equal(t, uint8(2), piece.Fr32PaddedSizeToV1TreeHeight(65)) // rounds up + + require.Equal(t, uint64(32)<<10, piece.HeightToPaddedSize(10)) + require.Equal(t, uint64(127), piece.MaxDataSize(128)) +} diff --git a/internal/piece/size.go b/internal/piece/size.go new file mode 100644 index 00000000..c2e9ed27 --- /dev/null +++ b/internal/piece/size.go @@ -0,0 +1,58 @@ +package piece + +import ( + "fmt" + "math/bits" +) + +// Fr32PaddedSizeToV1TreeHeight returns the binary-tree height for an +// FR32-padded piece of the given size. Sizes that aren't a power of two +// round up under the assumption the tree is padded with zeros. +func Fr32PaddedSizeToV1TreeHeight(size uint64) uint8 { + if size <= 32 { + return 0 + } + + b := 63 - bits.LeadingZeros64(size) + b -= 5 // 2^5 == 32-byte leaves + + if 32< SIGNING_SERVICE_* env > signer.yaml (cwd) > defaults + signer/ # Core EIP-712 signer: SignCreateDataSet, SignAddPieces, + # SignSchedulePieceRemovals, SignDeleteDataSet, Recover* helpers + types/ # SigningService + OperationSigner interfaces, legacy request/response types + handlers/ # Legacy REST handlers (/sign/* and /healthcheck) + server/ # UCAN 1.0 HTTP server construction (ucantone), DID resolvers + handlers/ # UCAN invocation handlers (access grant + the four sign commands) + inprocess/ # In-process SigningService impl (no network, no auth checks) — for tests/dev + client/ # Go client library: UCAN invocations against a remote signing service +deploy/ # Terraform/OpenTofu (generated with storoku); GH Actions deploys on release +signer.yaml.example # Full config template +``` + +## Configuration + +Priority: CLI flags > env vars (`SIGNING_SERVICE_` prefix) > `signer.yaml` in +the working directory > defaults (host `localhost`, port `7446` — "SIGN" on a +T9 keypad). + +Required: +- `rpc_url` — Ethereum RPC endpoint (chain ID is detected from it at startup) +- `service_contract_address` — FilecoinWarmStorageService contract +- ECDSA signing key: one of `signing_key` (hex), `signing_key_path`, or + `signing_keystore_path` + `signing_keystore_password` +- Service identity (UCAN issuer): `service_key` (multibase Ed25519) or + `service_key_file` (Ed25519 PEM) — exactly one, not both — plus `service_did` + (must be a did:web), which is required whenever a service key is set + +Dev-only hidden flag: `--insecure-did-resolution` enables did:web resolution +over plain HTTP (used in tests/local setups). + +Deployment note: non-secret production env vars (`SIGNING_SERVICE_RPC_URL`, +`SIGNING_SERVICE_SERVICE_CONTRACT_ADDRESS`) live in +`deploy/.env.production.local.tpl`. + +## HTTP API + +| Method | Path | Purpose | +|--------|------|---------| +| POST | `/` | UCAN invocation endpoint (primary; ucantone `server.HTTPServer` wrapped as an Echo handler) | +| GET | `/healthcheck` | Health: signer address, chain ID | +| POST | `/sign/create-dataset` | Legacy blind-sign CreateDataSet | +| POST | `/sign/add-pieces` | Legacy blind-sign AddPieces | +| POST | `/sign/schedule-piece-removals` | Legacy blind-sign SchedulePieceRemovals | +| POST | `/sign/delete-dataset` | Legacy blind-sign DeleteDataSet | + +The `/sign/*` routes are marked TODO-remove once all piri nodes have moved to +UCAN invocations — assume they may still be in use; don't remove or change +their wire format without checking deployed piri nodes. + +## UCAN Commands + +The service uses UCAN 1.0 via `github.com/fil-forge/ucantone` with command and +argument types defined in `github.com/fil-forge/forge/protocol` (`commands/access`, +`commands/pdp/sign`). Five commands are handled (`pkg/server/server.go`): + +| Command | Handler | Notes | +|---|---|---| +| `/access/grant` | `pkg/server/handlers/access_grant.go` | Issues 1-hour delegations, one per requested attenuation; only `/pdp/sign/*` commands are grantable; delegations attached to the response container, CIDs in the receipt OK | +| `/pdp/sign/dataset/create` | `pkg/server/handlers/sign.go` | | +| `/pdp/sign/dataset/delete` | `pkg/server/handlers/sign.go` | | +| `/pdp/sign/pieces/add` | `pkg/server/handlers/sign.go` | | +| `/pdp/sign/pieces/remove/schedule` | `pkg/server/handlers/sign.go` | | + +Sign handlers require the invocation subject to equal the service DID +(i.e. the caller must hold a delegation from the service — self-signed +invocations fail with an `InvalidResource` receipt error). + +Piri clients sign invocations as did:web identities, so the server registers a +did:web resolver (HTTPS did.json fetch, 3-hour cache; the service's own DID is +resolved from a well-known in-memory document). Without this the validator +only accepts did:key issuers. + +## Key Interfaces + +`pkg/types` defines two interfaces — keep them in sync when adding operations: + +- `OperationSigner` — the raw EIP-712 signing surface (`pkg/signer.Signer` + implements it; used by both legacy handlers and UCAN handlers) +- `SigningService` — the higher-level authorized-signing surface. Implemented + by `pkg/client.Client` (remote, UCAN invocations) and `pkg/inprocess.Signer` + (direct, no auth — testing/dev). Piri consumes this interface, so changes + here are breaking for piri. + +## Key Dependencies + +- `github.com/ethereum/go-ethereum` — ECDSA keys, keystore, RPC client +- `github.com/fil-forge/filecoin-services/go` — `eip712` typed-data signing utilities +- `github.com/fil-forge/ucantone` — UCAN 1.0 (server, client, delegation, DID resolvers) +- `github.com/fil-forge/forge/protocol` and `github.com/fil-forge/forge/internal` — shared UCAN command/argument schemas, service identity +- `github.com/labstack/echo/v4` — HTTP server; `spf13/cobra` + `viper` — CLI/config + +## Conventions + +- Testing: standard `go test` with testify (`assert`/`require`); tests live + next to the code (`*_test.go`) +- Logging: `ipfs/go-log/v2` in packages, Echo middleware logger for HTTP +- Go file naming: snake_case + +## Blast Radius / Gotchas + +- Changing EIP-712 message construction (`pkg/signer`, or the eip712 types it + uses) breaks on-chain PDP proof verification — the contract must recover + the expected signer from the exact typed data. +- The UCAN command/argument schemas are defined in the in-repo `protocol` module, not here. + Changing request/response shapes means coordinating a `protocol` change and + updating piri (the client side) as well. +- Key-loading changes affect every piri node's ability to obtain valid + signatures. +- Chain ID is baked into signatures via the EIP-712 domain; a wrong `rpc_url` + (wrong network) yields signatures the contract rejects. diff --git a/piri-signing-service/CLAUDE.md b/piri-signing-service/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/piri-signing-service/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/piri-signing-service/LICENSE b/piri-signing-service/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/piri-signing-service/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/piri-signing-service/Makefile b/piri-signing-service/Makefile new file mode 100644 index 00000000..2c600f17 --- /dev/null +++ b/piri-signing-service/Makefile @@ -0,0 +1,97 @@ +# Signing Service Makefile +BINARY_NAME=signer +VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") +DATE=$(shell date -u -Iseconds) +GOFLAGS=-ldflags="-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.date=$(DATE)" + +.PHONY: all build test clean install help + +# Default target +all: build + +# Build the signing service binary +build: $(BINARY_NAME) + +$(BINARY_NAME): FORCE + @if [ ! -f $(BINARY_NAME) ] || \ + [ -n "$$(find . -name '*.go' -type f -newer $(BINARY_NAME) 2>/dev/null)" ]; then \ + echo "Building $(BINARY_NAME)..."; \ + go build $(GOFLAGS) -o $(BINARY_NAME) .; \ + else \ + echo "$(BINARY_NAME) is up to date"; \ + fi + +FORCE: + +# Run tests +test: + go test -v ./... + +# Run tests with coverage +test-coverage: + go test -v -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + @echo "Coverage report generated: coverage.html" + +# Clean build artifacts +clean: + rm -f $(BINARY_NAME) + go clean + +# Install the binary to GOPATH/bin +install: + go install $(GOFLAGS) . + +# Run the signer (for development) +run: build + ./$(BINARY_NAME) + +# Format code +fmt: + go fmt ./... + +# Lint code +lint: + golangci-lint run + +# Tidy dependencies +tidy: + go mod tidy + +# Show help +help: + @echo "Signing Service Makefile" + @echo "" + @echo "Usage:" + @echo " make Build the signing service" + @echo " make build Build the signing service" + @echo " make test Run tests" + @echo " make test-coverage Run tests with coverage report" + @echo " make clean Remove build artifacts" + @echo " make install Install binary to GOPATH/bin" + @echo " make run Build and run the signer" + @echo " make fmt Format code" + @echo " make lint Lint code (requires golangci-lint)" + @echo " make tidy Tidy go.mod dependencies" + @echo " make help Show this help message" + +# --- monorepo verb contract (see the root Makefile): vet, image, image-dev --- +# Resolve the docker binary to an absolute path: directories named 'docker' +# on PATH (including this repo's own docker/ when '.' is in PATH) break make's +# direct exec of a bare 'docker'. +DOCKER := $(shell command -v docker) + +# Container image via the monorepo-shared docker/Dockerfile (context = the repo root). +IMAGE_BUILD_ARGS := --build-arg SERVICE=piri-signing-service --build-arg MAIN_PKG=. --build-arg BIN=signer + +.PHONY: vet image image-dev +vet: + go vet ./... + +image: + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/piri-signing-service:local .. + +image-dev: + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/piri-signing-service:local-dev .. + diff --git a/piri-signing-service/README.md b/piri-signing-service/README.md new file mode 100644 index 00000000..1d404822 --- /dev/null +++ b/piri-signing-service/README.md @@ -0,0 +1,363 @@ +# Piri Signing Service + +A secure HTTP signing service for Storacha's Proof of Data Possession (PDP) operations on Filecoin. + +## Why This Service Exists + +The Piri Signing Service acts as a secure bridge between Storacha's cold wallet and the PDP verification system on Filecoin. Instead of exposing private keys directly to multiple piri nodes, this service provides a centralized, auditable signing endpoint that: + +1. **Protects Storacha's private keys** - Keys never leave the signing service +2. **Enables distributed operations** - Multiple piri nodes can request signatures without key access +3. **Provides audit trails** - All signing operations can be logged and monitored +4. **Supports future authentication** - Designed to evolve from blind signing to authenticated operations + +## Quick Start + +### 1. Install the Service + +```bash +go install github.com/fil-forge/forge/piri-signing-service@latest +``` + +Or build from source: + +```bash +git clone https://github.com/fil-forge/piri-signing-service.git +cd piri-signing-service +go build -o signing-service +``` + +### 2. Configure + +Create a `signer.yaml` file in your working directory: + +```yaml +# Network configuration +host: localhost +port: 7446 # Spells SIGN on T9 keyboard + +# Ethereum RPC endpoint (required) +rpc_url: https://api.calibration.node.glif.io/rpc/v1 + +# Contract address (required) +service_contract_address: "0x8b7aa0a68f5717e400F1C4D37F7a28f84f76dF91" + +# Private key configuration (choose one) +signing_key: "0x0000000000000000000000000000000000000000" +# OR +# signing_key_path: /secure/path/to/key.hex +# OR +# signing_keystore_path: /secure/path/to/keystore.json +# signing_keystore_password: your-password +``` + +See `signer.yaml.example` for full configuration options. + +### 3. Run + +```bash +./signing-service +``` + +The service will start and display: +- Signer address (the address that will sign operations) +- Chain ID (detected from RPC endpoint) +- Contract address (for verification) +- Listening endpoint + +## Configuration + +The service supports multiple configuration methods with the following priority: + +1. **Command-line flags** (highest priority) +2. **Environment variables** +3. **Configuration file** (`signer.yaml`) +4. **Default values** (only for host and port) + +### Configuration Methods + +#### Via Configuration File + +Place a `signer.yaml` file in the current directory. See `signer.yaml.example` for a complete template. + +#### Via Environment Variables + +All configuration can be set via environment variables with the `SIGNING_SERVICE_` prefix: + +```bash +export SIGNING_SERVICE_RPC_URL=https://api.calibration.node.glif.io/rpc/v1 +export SIGNING_SERVICE_SERVICE_CONTRACT_ADDRESS=0x8b7aa0a68f5717e400F1C4D37F7a28f84f76dF91 +export SIGNING_SERVICE_SIGNING_KEY_PATH=/secure/path/to/key.hex +``` + +#### Via Command-line Flags + +```bash +./signing-service \ + --rpc-url=https://api.calibration.node.glif.io/rpc/v1 \ + --contract-address=0x8b7aa0a68f5717e400F1C4D37F7a28f84f76dF91 \ + --signing-key-path=/secure/path/to/key.hex +``` + +### Required Configuration + +The following must be provided (no defaults): +- `rpc_url` - Ethereum RPC endpoint +- `service_contract_address` - FilecoinWarmStorageService contract address +- Either `signing_key` OR `signing_key_path` OR `signing_keystore_path` + `signing_keystore_password` + + +## Service deployment + +This project contains Terraform/OpenTofu deployment scripts for the service, generated using the [storoku](https://github.com/storacha/storoku) tool. The scripts are located in the [deploy](/deploy) directory. + +A GitHub Actions workflow will deploy the service when a new release is created. These deployments can also be triggered manually from a development environment. + +Required environment variables **that are not secrets** are set in [deploy/.env.production.local.tpl](/deploy/.env.production.local.tpl). More specifically, `rpc_url` and `service_contract_address` are defined there (as `SIGNING_SERVICE_RPC_URL` and `SIGNING_SERVICE_SERVICE_CONTRACT_ADDRESS`, respectively). + +## Container images + +A push to `main` publishes to GHCR from the `Container` workflow, as +`ghcr.io/fil-forge/piri-signing-service:main` plus a `sha-` tag, +covering `linux/amd64` and `linux/arm64`. Release tags publish the same image +under their semver version. + +## Deploying to Forge Central dev + +The `main` run also asks [infra-central][] to deploy what it just published. It +dispatches a `bump-deployed-image` event carrying the manifest digest, and +infra-central's [Bump deployed image][receiver] workflow opens a pull request +pinning that digest in `terraform/envs/dev/apps/terraform.tfvars`, with +auto-merge enabled. infra-central's [Check and deploy][deploy] workflow runs +`tofu apply` on `dev/apps` on every push to its `main`, so merging that pull +request is what deploys. + +The dispatch runs as the `fil-forge-bot` GitHub App and needs the +`FORGE_BOT_APP_ID` variable and the `FORGE_BOT_PRIVATE_KEY` secret. Prod pins +are promoted by hand. This is separate from the storoku deployment described +above, which targets its own environments. + +[infra-central]: https://github.com/fil-forge/infra-central +[receiver]: https://github.com/fil-forge/infra-central/blob/main/.github/workflows/bump-deployed-image.yml +[deploy]: https://github.com/fil-forge/infra-central/blob/main/.github/workflows/check-and-deploy.yml + +## API Endpoints + +### Health Check + +```bash +curl http://localhost:7446/healthcheck +``` + +### Sign Create Dataset + +```bash +curl -X POST http://localhost:7446/sign/create-dataset \ + -H "Content-Type: application/json" \ + -d '{ + "clientDatasetId": "1", + "payeeAddress": "0xYourPayeeAddress", + "metadata": { + "key1": "value1", + "key2": "value2" + } + }' +``` + +### Sign Add Pieces + +```bash +curl -X POST http://localhost:7446/sign/add-pieces \ + -H "Content-Type: application/json" \ + -d '{ + "clientDatasetId": "1", + "pieces": ["piece1", "piece2"] + }' +``` + +### Sign Schedule Piece Removals + +```bash +curl -X POST http://localhost:7446/sign/schedule-piece-removals \ + -H "Content-Type: application/json" \ + -d '{ + "clientDatasetId": "1", + "pieces": ["piece1", "piece2"] + }' +``` + +### Sign Delete Dataset + +```bash +curl -X POST http://localhost:7446/sign/delete-dataset \ + -H "Content-Type: application/json" \ + -d '{ + "clientDatasetId": "1" + }' +``` + +## Security Roadmap + +The service is designed to evolve through multiple security phases: + +### Phase 1: Blind Signing (Current) +- Signs any properly formatted request +- No authentication required +- Suitable for trusted, private networks only +- **⚠️ WARNING: Do not expose to public internet** + +### Phase 2: UCAN Authentication (Planned) +- Registered operators authenticate via UCAN tokens +- Each operator has specific permissions +- Audit trail of who requested each signature + +### Phase 3: Session Keys (Future) +- Temporary session keys for time-limited operations +- Reduced exposure of primary signing key +- Automatic key rotation + +### Phase 4: Hardware Security Module (Future) +- Cold wallet replaced with HSM or cloud KMS +- Hardware-level key protection +- Compliance with security standards + +## Production Deployment + +### Recommended Setup + +1. **Run on a secure, isolated network** - Not exposed to public internet +2. **Use environment variables or secure key management** for sensitive configuration +3. **Enable comprehensive logging** for audit trails +4. **Monitor all signing requests** for anomalies +5. **Implement rate limiting** at the network level +6. **Use TLS/HTTPS** for all communications + +### Example Systemd Service + +```ini +[Unit] +Description=Piri Signing Service +After=network.target + +[Service] +Type=simple +User=signing-service +WorkingDirectory=/opt/signing-service +ExecStart=/opt/signing-service/signing-service +Restart=always +RestartSec=10 + +# Security hardening +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/signing-service/logs + +# Environment variables +Environment="SIGNING_SERVICE_RPC_URL=https://api.node.glif.io/rpc/v1" +EnvironmentFile=/etc/signing-service/env + +[Install] +WantedBy=multi-user.target +``` + +### Docker Deployment + +```dockerfile +FROM golang:1.21-alpine AS builder +WORKDIR /app +COPY . . +RUN go build -o signing-service + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root/ +COPY --from=builder /app/signing-service . +EXPOSE 7446 +CMD ["./signing-service"] +``` + +```bash +docker run -d \ + -p 7446:7446 \ + -v /secure/path/to/config:/root/signer.yaml:ro \ + -v /secure/path/to/keys:/keys:ro \ + --name signing-service \ + fil-forge/piri-signing-service +``` + +## Monitoring + +### Key Metrics to Monitor + +- Request rate per endpoint +- Response times +- Error rates +- Unique requesters (when authentication is added) +- Gas price at signature time +- Contract verification failures + +### Example Prometheus Metrics + +The service exposes metrics at `/metrics` (when enabled): + +``` +signing_service_requests_total{endpoint="create_dataset"} +signing_service_request_duration_seconds{endpoint="create_dataset"} +signing_service_errors_total{endpoint="create_dataset",error="validation_failed"} +signing_service_signer_address{address="0x..."} +``` + +## Troubleshooting + +### Common Issues + +**Service won't start** +- Check all required configuration is provided +- Verify RPC endpoint is reachable +- Ensure private key file has correct permissions (600) + +**"Invalid contract address"** +- Verify the contract address is a valid Ethereum address +- Ensure it matches the deployed FilecoinWarmStorageService contract + +**"Failed to get chain ID"** +- Check RPC endpoint is correct and accessible +- Verify network connectivity + +**"Invalid signature" from contract** +- Ensure chain ID matches the network +- Verify contract address is correct +- Check that the signer address has appropriate permissions + +## Development + +### Running Tests + +```bash +go test ./... +``` + +### Building for Different Platforms + +```bash +# Linux +GOOS=linux GOARCH=amd64 go build -o signing-service-linux + +# macOS +GOOS=darwin GOARCH=amd64 go build -o signing-service-darwin + +# Windows +GOOS=windows GOARCH=amd64 go build -o signing-service.exe +``` + +## Support + +For issues, feature requests, or questions: +- GitHub Issues: https://github.com/fil-forge/forge/piri-signing-service/issues +- Documentation: https://docs.storacha.com/signing-service + +## License + +[License details here] \ No newline at end of file diff --git a/piri-signing-service/go.mod b/piri-signing-service/go.mod new file mode 100644 index 00000000..e4bdef95 --- /dev/null +++ b/piri-signing-service/go.mod @@ -0,0 +1,92 @@ +module github.com/fil-forge/forge/piri-signing-service + +go 1.27.0 + +require ( + github.com/ethereum/go-ethereum v1.16.7 + github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 + github.com/fil-forge/forge/internal v0.0.0 + github.com/fil-forge/forge/protocol v0.0.0 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 + github.com/ipfs/go-cid v0.6.2 + github.com/ipfs/go-log/v2 v2.9.2 + github.com/labstack/echo/v4 v4.13.4 + github.com/labstack/gommon v0.4.2 + github.com/spf13/cobra v1.10.2 + github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.12.1 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/alanshaw/dag-json-gen v0.0.9 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multibase v0.3.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect + github.com/multiformats/go-multihash v0.2.3 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/prometheus/client_golang v1.23.0 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/whyrusleeping/cbor-gen v0.3.1 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/time v0.12.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + lukechampine.com/blake3 v1.4.1 // indirect + pitr.ca/jsontokenizer v0.3.2 // indirect +) + +replace github.com/fil-forge/forge/protocol => ../protocol + +replace github.com/fil-forge/forge/internal => ../internal diff --git a/piri-signing-service/go.sum b/piri-signing-service/go.sum new file mode 100644 index 00000000..8dfa2934 --- /dev/null +++ b/piri-signing-service/go.sum @@ -0,0 +1,354 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +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/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/alanshaw/dag-json-gen v0.0.9 h1:q59Ra1mQB6HnFGI1ilFxsZvF1CqGN0XDlUWq16EJ6pw= +github.com/alanshaw/dag-json-gen v0.0.9/go.mod h1:v1YBZcS4B355MqxtyQr+fGNbEhm0CzHd+gOqOO/MZ+I= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +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/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= +github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 h1:h7SZVboCO1iPDtdxPJjs1fj4IVFq/dpDWFevkXtwgps= +github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390/go.mod h1:ur5fRrG8v9twwwRFdX7lquGza4liT6x/xAFW7T4XVIc= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +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.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= +github.com/ipfs/go-log/v2 v2.9.2 h1:O/5BB0elpkRILvT24rCJ5976wWd7u0nJ436T3rdYdc4= +github.com/ipfs/go-log/v2 v2.9.2/go.mod h1:RziRwwXWhndlk8L75RnEe0zeAYaq2heKtEMc3jqUov0= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/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.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +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/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/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/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +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-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +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/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +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.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/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/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +pitr.ca/jsontokenizer v0.3.2 h1:kzsKwfkWPV5XCmAF//hznh3L9IXbiL8aHbI0FswRkCs= +pitr.ca/jsontokenizer v0.3.2/go.mod h1:3DJdA2QNOU6cI0XkH6pRKZ4Oe8G5SDRUQ6PFAwaQ3YY= diff --git a/piri-signing-service/main.go b/piri-signing-service/main.go new file mode 100644 index 00000000..730ab118 --- /dev/null +++ b/piri-signing-service/main.go @@ -0,0 +1,220 @@ +package main + +import ( + "context" + "crypto/ecdsa" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/ethereum/go-ethereum/ethclient" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/labstack/gommon/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/internal/pdpsigner/signer" + "github.com/fil-forge/forge/piri-signing-service/pkg/config" + "github.com/fil-forge/forge/piri-signing-service/pkg/handlers" + "github.com/fil-forge/forge/piri-signing-service/pkg/server" +) + +var rootCmd = &cobra.Command{ + Use: "signing-service", + Short: "HTTP service for signing PDP operations on behalf of Storacha", + Long: `A signing service that accepts PDP operation payloads via HTTP and returns +EIP-712 signatures. This service wraps the signer.Signer and provides a REST API +for piri nodes to request signatures without exposing Storacha's private key. + +Phase 1 (current): Blindly signs any request (no authentication) +Phase 2 (future): UCAN authentication for registered operators +Phase 3 (future): Session key integration +Phase 4 (future): Replace cold wallet with session key`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Initialize Viper with the command's flags + return config.Init() + }, + RunE: run, +} + +func init() { + // NB: on T9 keyboard 7446 spells SIGN :) + rootCmd.Flags().String("host", config.DefaultHost, "Host to listen on") + cobra.CheckErr(viper.BindPFlag("host", rootCmd.Flags().Lookup("host"))) + + rootCmd.Flags().Int("port", config.DefaultPort, "HTTP server port") + cobra.CheckErr(viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))) + + rootCmd.Flags().String("rpc-url", "", "Ethereum RPC URL") + cobra.CheckErr(viper.BindPFlag("rpc_url", rootCmd.Flags().Lookup("rpc-url"))) + + rootCmd.Flags().String("service-contract-address", "", "FilecoinWarmStorageService contract address") + cobra.CheckErr(viper.BindPFlag("service_contract_address", rootCmd.Flags().Lookup("service-contract-address"))) + + rootCmd.Flags().String("service-key", "", "Multibase-encoded private service key string") + cobra.CheckErr(viper.BindPFlag("service_key", rootCmd.Flags().Lookup("service-key"))) + + rootCmd.Flags().String("service-key-file", "", "Path to Ed25519 PEM key file for service identity") + cobra.CheckErr(viper.BindPFlag("service_key_file", rootCmd.Flags().Lookup("service-key-file"))) + rootCmd.MarkFlagsMutuallyExclusive("service-key", "service-key-file") + + rootCmd.Flags().String("service-did", "", "A DID web that identifies this service publicly") + cobra.CheckErr(viper.BindPFlag("service_did", rootCmd.Flags().Lookup("service-did"))) + + rootCmd.Flags().String("signing-key", "", "Hex-encoded private signing key string") + cobra.CheckErr(viper.BindPFlag("signing_key", rootCmd.Flags().Lookup("signing-key"))) + + rootCmd.Flags().String("signing-key-path", "", "Path to private signing key file") + cobra.CheckErr(viper.BindPFlag("signing_key_path", rootCmd.Flags().Lookup("signing-key-path"))) + + rootCmd.Flags().String("signing-keystore-path", "", "Path to signing keystore file") + cobra.CheckErr(viper.BindPFlag("signing_keystore_path", rootCmd.Flags().Lookup("signing-keystore-path"))) + + rootCmd.Flags().String("signing-keystore-password", "", "Signing keystore password") + cobra.CheckErr(viper.BindPFlag("signing_keystore_password", rootCmd.Flags().Lookup("signing-keystore-password"))) + + rootCmd.Flags().Bool("insecure-did-resolution", false, "Enable insecure did resolution over http") + cobra.CheckErr(viper.BindPFlag("insecure_did_resolution", rootCmd.Flags().Lookup("insecure-did-resolution"))) + cobra.CheckErr(rootCmd.Flags().MarkHidden("insecure-did-resolution")) +} + +func run(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + // Load configuration from Viper (which already has flags, env vars, and config file values) + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("loading configuration: %w", err) + } + + // Load service identity + var id identity.Identity + switch { + case cfg.ServiceKeyFile != "": + id, err = identity.NewFromPEMFileWithDID(cfg.ServiceKeyFile, cfg.ServiceDID) + if err != nil { + return fmt.Errorf("loading service identity from file: %w", err) + } + case cfg.ServiceKey != "": + id, err = identity.New(cfg.ServiceKey, cfg.ServiceDID) + if err != nil { + return fmt.Errorf("loading service identity: %w", err) + } + default: + return fmt.Errorf("either service_key or service_key_file must be provided") + } + + // Load private signing key + var signingKey *ecdsa.PrivateKey + switch { + case cfg.SigningKey != "": + signingKey, err = config.LoadSigningKey(cfg.SigningKey) + if err != nil { + return fmt.Errorf("loading signing key: %w", err) + } + case cfg.SigningKeyPath != "": + signingKey, err = config.LoadSigningKeyFromFile(cfg.SigningKeyPath) + if err != nil { + return fmt.Errorf("loading signing key from file: %w", err) + } + default: + signingKey, err = config.LoadSigningKeyFromKeystore(cfg.SigningKeystorePath, cfg.SigningKeystorePassword) + if err != nil { + return fmt.Errorf("loading signing keystore: %w", err) + } + } + + // Connect to RPC to get chain ID + client, err := ethclient.Dial(cfg.RPCUrl) + if err != nil { + return fmt.Errorf("connecting to RPC endpoint: %w", err) + } + defer client.Close() + + chainID, err := client.ChainID(ctx) + if err != nil { + return fmt.Errorf("getting chain ID: %w", err) + } + + // Create EIP-712 signer + s := signer.NewSigner(signingKey, chainID, cfg.ContractAddr()) + + // Create Echo instance + e := echo.New() + e.HideBanner = true + e.HidePort = true + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Logger.SetLevel(log.DEBUG) + + // Create HTTP handlers + handler := handlers.NewHandler(s) + + // Create UCAN server (UCAN 1.0 via ucantone + the protocol module) + var ucanSvrOpts []server.Option + if cfg.InsecureDIDResolution { + ucanSvrOpts = append(ucanSvrOpts, server.WithInsecureDIDResolution()) + } + ucanSrv, err := server.New(id, s, ucanSvrOpts...) + if err != nil { + return fmt.Errorf("creating UCAN server: %w", err) + } + + // Setup routes — the ucantone server is an http.Handler, route POST / to it. + e.POST("/", echo.WrapHandler(ucanSrv)) + e.GET("/healthcheck", handler.Health) + // TODO: remove /sign/* routes after all nodes transition to UCAN invocations + e.POST("/sign/create-dataset", handler.SignCreateDataSet) + e.POST("/sign/add-pieces", handler.SignAddPieces) + e.POST("/sign/schedule-piece-removals", handler.SignSchedulePieceRemovals) + e.POST("/sign/delete-dataset", handler.SignDeleteDataSet) + + // Log startup info + cmd.Println("Signing service starting...") + cmd.Printf(" Service ID: %s\n", id.DID()) + cmd.Printf(" Signer address: %s\n", s.GetAddress().Hex()) + cmd.Printf(" Chain ID: %s\n", chainID.String()) + cmd.Printf(" Verifying contract: %s\n", cfg.ServiceContractAddress) + cmd.Printf(" Host: %s\n", cfg.Host) + cmd.Printf(" Port: %d\n", cfg.Port) + + // Start server in goroutine + go func() { + e.Logger.Infof("✓ Server listening on http://%s:%d", cfg.Host, cfg.Port) + if err := e.Start(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)); err != nil && err != http.ErrServerClosed { + e.Logger.Fatal("shutting down the server") + } + }() + + // Wait for interrupt signal + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + e.Logger.Info("Shutting down server...") + + // Graceful shutdown + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := e.Shutdown(ctx); err != nil { + return fmt.Errorf("server shutdown error: %w", err) + } + + e.Logger.Info("Server stopped") + return nil +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} diff --git a/piri-signing-service/pkg/config/config.go b/piri-signing-service/pkg/config/config.go new file mode 100644 index 00000000..cfd0c421 --- /dev/null +++ b/piri-signing-service/pkg/config/config.go @@ -0,0 +1,192 @@ +package config + +import ( + "crypto/ecdsa" + "encoding/hex" + "errors" + "fmt" + "os" + "strings" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/spf13/viper" +) + +const ( + // Default values - only host and port have defaults + DefaultHost = "localhost" + DefaultPort = 7446 + + // Environment variable prefix + EnvPrefix = "SIGNING_SERVICE" + + // Config file name (without extension) + ConfigFileName = "signer" +) + +type Config struct { + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + RPCUrl string `mapstructure:"rpc_url"` + ServiceContractAddress string `mapstructure:"service_contract_address"` + ServiceKey string `mapstructure:"service_key"` + ServiceKeyFile string `mapstructure:"service_key_file"` + ServiceDID string `mapstructure:"service_did"` + SigningKey string `mapstructure:"signing_key"` + SigningKeyPath string `mapstructure:"signing_key_path"` + SigningKeystorePath string `mapstructure:"signing_keystore_path"` + SigningKeystorePassword string `mapstructure:"signing_keystore_password"` + + // development server settings + InsecureDIDResolution bool `mapstructure:"insecure_did_resolution"` +} + +// InitViper initializes Viper with defaults and binds it to Cobra flags +func Init() error { + // Set up environment variables + viper.SetEnvPrefix(EnvPrefix) + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() + + // Set up config file + viper.SetConfigName(ConfigFileName) + viper.SetConfigType("yaml") + viper.AddConfigPath(".") // Look in current directory + + // Try to read config file (don't fail if not found) + if err := viper.ReadInConfig(); err != nil { + // It's OK if the config file doesn't exist + var configFileNotFoundError viper.ConfigFileNotFoundError + if !errors.As(err, &configFileNotFoundError) { + // But return other errors (like parse errors) + return fmt.Errorf("error reading config file: %w", err) + } + } + + return nil +} + +// Load reads configuration from viper and validates it +func Load() (*Config, error) { + var cfg Config + + // Unmarshal config from viper + if err := viper.Unmarshal(&cfg); err != nil { + return nil, fmt.Errorf("unmarshaling config: %w", err) + } + + // Validate the configuration + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("config validation failed: %w", err) + } + + return &cfg, nil +} + +// Validate checks that required configuration fields are set +func (c *Config) Validate() error { + // Host and port have defaults, so they should always be set + if c.Host == "" { + return fmt.Errorf("host is required") + } + + if c.Port <= 0 || c.Port > 65535 { + return fmt.Errorf("port must be between 1 and 65535") + } + + // These fields are required and have no defaults + if c.RPCUrl == "" { + return fmt.Errorf("rpc_url is required (set via flag --rpc-url, env SIGNING_SERVICE_RPC_URL, or in signer.yaml)") + } + + if c.ServiceContractAddress == "" { + return fmt.Errorf("service_contract_address is required (set via flag --contract-address, env SIGNING_SERVICE_SERVICE_CONTRACT_ADDRESS, or in signer.yaml)") + } + + if !common.IsHexAddress(c.ServiceContractAddress) { + return fmt.Errorf("invalid contract address: %s", c.ServiceContractAddress) + } + + // mutually exclusive configuration values + if c.ServiceKey != "" && c.ServiceKeyFile != "" { + return fmt.Errorf("only one of service_key or service_key_file can be specified") + } + // If using service key or service key file, a did web is required + if c.ServiceKey != "" || c.ServiceKeyFile != "" { + if c.ServiceDID == "" { + return fmt.Errorf("service_did is required when using service_key or service_key_file") + } + + if !strings.HasPrefix(c.ServiceDID, "did:web:") { + return fmt.Errorf("service_did must be a did:web") + } + } + + // Must have either signing key, signing key file or keystore + if c.SigningKey == "" && c.SigningKeyPath == "" && c.SigningKeystorePath == "" { + return fmt.Errorf("either signing_key, signing_key_path or signing_keystore_path must be provided (set via flags, env vars, or in signer.yaml)") + } + + // If using keystore, password is required + if c.SigningKeystorePath != "" && c.SigningKeystorePassword == "" { + return fmt.Errorf("signing_keystore_password is required when using signing_keystore_path") + } + + return nil +} + +// ContractAddr returns the contract address as a common.Address +func (c *Config) ContractAddr() common.Address { + return common.HexToAddress(c.ServiceContractAddress) +} + +// LoadSigningKey loads a signing key from a string +// The byte slice can contain either hex-encoded or raw bytes +func LoadSigningKey(data string) (*ecdsa.PrivateKey, error) { + // Trim whitespace + keyData := strings.TrimSpace(data) + + // Try hex decoding first + keyData = strings.TrimPrefix(keyData, "0x") + + keyBytes, err := hex.DecodeString(keyData) + if err != nil { + // If hex decoding fails, try using the raw bytes + keyBytes = []byte(data) + } + + key, err := crypto.ToECDSA(keyBytes) + if err != nil { + return nil, fmt.Errorf("parsing signing key: %w", err) + } + + return key, nil +} + +// LoadSigningKeyFromFile loads a signing key from a file +// The file can contain either hex-encoded or raw bytes +func LoadSigningKeyFromFile(path string) (*ecdsa.PrivateKey, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading signing key file: %w", err) + } + + return LoadSigningKey(string(data)) +} + +// LoadSigningKeyFromKeystore loads a signing key from an encrypted keystore file +func LoadSigningKeyFromKeystore(keystorePath, password string) (*ecdsa.PrivateKey, error) { + keystoreJSON, err := os.ReadFile(keystorePath) + if err != nil { + return nil, fmt.Errorf("reading signing keystore file: %w", err) + } + + key, err := keystore.DecryptKey(keystoreJSON, password) + if err != nil { + return nil, fmt.Errorf("decrypting signing keystore: %w", err) + } + + return key.PrivateKey, nil +} diff --git a/piri-signing-service/pkg/handlers/handlers.go b/piri-signing-service/pkg/handlers/handlers.go new file mode 100644 index 00000000..570a54d7 --- /dev/null +++ b/piri-signing-service/pkg/handlers/handlers.go @@ -0,0 +1,177 @@ +package handlers + +import ( + "fmt" + "math/big" + "net/http" + + "github.com/ethereum/go-ethereum/common" + "github.com/labstack/echo/v4" + + "github.com/fil-forge/forge/internal/pdpsigner/signer" + "github.com/fil-forge/forge/internal/pdpsigner/types" +) + +// Handler wraps the EIP-712 signer and provides HTTP endpoints +type Handler struct { + signer *signer.Signer +} + +// NewHandler creates a new HTTP handler +func NewHandler(s *signer.Signer) *Handler { + return &Handler{signer: s} +} + +// Health returns service health status +func (h *Handler) Health(c echo.Context) error { + response := types.HealthResponse{ + Status: "healthy", + Signer: h.signer.GetAddress().Hex(), + } + return c.JSON(http.StatusOK, response) +} + +// SignCreateDataSet handles POST /sign/create-dataset +func (h *Handler) SignCreateDataSet(c echo.Context) error { + var req types.CreateDataSetRequest + if err := c.Bind(&req); err != nil { + c.Logger().Errorf("Error decoding request: %v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + } + + // Parse clientDataSetId + clientDataSetId, ok := new(big.Int).SetString(req.ClientDataSetId, 10) + if !ok { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid clientDataSetId") + } + + // Parse payee address + if !common.IsHexAddress(req.Payee) { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid payee address") + } + payee := common.HexToAddress(req.Payee) + + // Sign the data + signature, err := h.signer.SignCreateDataSet(clientDataSetId, payee, req.Metadata) + if err != nil { + c.Logger().Errorf("Error signing CreateDataSet: %v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Signing error: %v", err)) + } + + // Verify signature by recovering signer + recoveredSigner, err := h.signer.RecoverCreateDataSetSigner(clientDataSetId, payee, req.Metadata, signature) + if err != nil { + c.Logger().Errorf("Failed to recover signer: %v", err) + } else { + c.Logger().Infof("Signature verification - Expected: %s, Recovered: %s, Match: %v", + signature.Signer.Hex(), recoveredSigner.Hex(), signature.Signer == recoveredSigner) + } + + c.Logger().Infof("Signed CreateDataSet - datasetId=%s, payee=%s, signer=%s", + req.ClientDataSetId, req.Payee, signature.Signer.Hex()) + + return c.JSON(http.StatusOK, signature) +} + +// SignAddPieces handles POST /sign/add-pieces +func (h *Handler) SignAddPieces(c echo.Context) error { + var req types.AddPiecesRequest + if err := c.Bind(&req); err != nil { + c.Logger().Errorf("Error decoding request: %v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + } + + // Parse clientDataSetId + clientDataSetId, ok := new(big.Int).SetString(req.ClientDataSetId, 10) + if !ok { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid clientDataSetId") + } + + // Parse nonce + nonce, ok := new(big.Int).SetString(req.Nonce, 10) + if !ok { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid nonce/firstAdded") + } + + // Parse piece data (convert hex strings to bytes) + pieceData := make([][]byte, len(req.PieceData)) + for i, hexData := range req.PieceData { + pieceData[i] = common.FromHex(hexData) + } + + // Sign the data + signature, err := h.signer.SignAddPieces(clientDataSetId, nonce, pieceData, req.Metadata) + if err != nil { + c.Logger().Errorf("Error signing AddPieces: %v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Signing error: %v", err)) + } + + c.Logger().Infof("Signed AddPieces - datasetId=%s, pieces=%d, signer=%s", + req.ClientDataSetId, len(req.PieceData), signature.Signer.Hex()) + + return c.JSON(http.StatusOK, signature) +} + +// SignSchedulePieceRemovals handles POST /sign/schedule-piece-removals +func (h *Handler) SignSchedulePieceRemovals(c echo.Context) error { + var req types.SchedulePieceRemovalsRequest + if err := c.Bind(&req); err != nil { + c.Logger().Errorf("Error decoding request: %v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + } + + // Parse clientDataSetId + clientDataSetId, ok := new(big.Int).SetString(req.ClientDataSetId, 10) + if !ok { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid clientDataSetId") + } + + // Parse piece IDs + pieceIds := make([]*big.Int, len(req.PieceIds)) + for i, idStr := range req.PieceIds { + id, ok := new(big.Int).SetString(idStr, 10) + if !ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid pieceId at index %d", i)) + } + pieceIds[i] = id + } + + // Sign the data + signature, err := h.signer.SignSchedulePieceRemovals(clientDataSetId, pieceIds) + if err != nil { + c.Logger().Errorf("Error signing SchedulePieceRemovals: %v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Signing error: %v", err)) + } + + c.Logger().Infof("Signed SchedulePieceRemovals - datasetId=%s, pieces=%d, signer=%s", + req.ClientDataSetId, len(req.PieceIds), signature.Signer.Hex()) + + return c.JSON(http.StatusOK, signature) +} + +// SignDeleteDataSet handles POST /sign/delete-dataset +func (h *Handler) SignDeleteDataSet(c echo.Context) error { + var req types.DeleteDataSetRequest + if err := c.Bind(&req); err != nil { + c.Logger().Errorf("Error decoding request: %v", err) + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid request: %v", err)) + } + + // Parse clientDataSetId + clientDataSetId, ok := new(big.Int).SetString(req.ClientDataSetId, 10) + if !ok { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid clientDataSetId") + } + + // Sign the data + signature, err := h.signer.SignDeleteDataSet(clientDataSetId) + if err != nil { + c.Logger().Errorf("Error signing DeleteDataSet: %v", err) + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Signing error: %v", err)) + } + + c.Logger().Infof("Signed DeleteDataSet - datasetId=%s, signer=%s", + req.ClientDataSetId, signature.Signer.Hex()) + + return c.JSON(http.StatusOK, signature) +} diff --git a/piri-signing-service/pkg/handlers/handlers_test.go b/piri-signing-service/pkg/handlers/handlers_test.go new file mode 100644 index 00000000..3a84bc85 --- /dev/null +++ b/piri-signing-service/pkg/handlers/handlers_test.go @@ -0,0 +1,489 @@ +package handlers + +import ( + "encoding/json" + "errors" + "math/big" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/fil-forge/filecoin-services/go/eip712" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/fil-forge/forge/internal/pdpsigner/signer" + "github.com/fil-forge/forge/internal/pdpsigner/types" +) + +// createTestSigner creates a test signer with a random key +func createTestSigner(t *testing.T) *signer.Signer { + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + + chainID := big.NewInt(314159) // Calibration testnet + contractAddr := common.HexToAddress("0x8b7aa0a68f5717e400F1C4D37F7a28f84f76dF91") + + return signer.NewSigner(privateKey, chainID, contractAddr) +} + +func TestHealth(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + req := httptest.NewRequest(http.MethodGet, "/healthcheck", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := handler.Health(c) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, rec.Code) + + var response map[string]interface{} + err = json.Unmarshal(rec.Body.Bytes(), &response) + require.NoError(t, err) + + assert.Equal(t, "healthy", response["status"]) + assert.Equal(t, s.GetAddress().Hex(), response["signer"]) +} + +func TestSignCreateDataSet_Success(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + // Use a valid checksummed address + testPayee := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb") + + requestBody := types.CreateDataSetRequest{ + ClientDataSetId: "123", + Payee: testPayee.Hex(), + Metadata: []eip712.MetadataEntry{ + {Key: "name", Value: "test-dataset"}, + {Key: "version", Value: "1.0"}, + }, + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/create-dataset", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignCreateDataSet(c) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, rec.Code) + + var signature eip712.AuthSignature + err = json.Unmarshal(rec.Body.Bytes(), &signature) + require.NoError(t, err) + + // Verify signature components + assert.NotEmpty(t, signature.Signature) + assert.Equal(t, s.GetAddress(), signature.Signer) + assert.NotEmpty(t, signature.SignedData) + assert.True(t, signature.V == 27 || signature.V == 28, "V should be 27 or 28") +} + +func TestSignCreateDataSet_InvalidJSON(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + req := httptest.NewRequest(http.MethodPost, "/sign/create-dataset", strings.NewReader("invalid json")) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err := handler.SignCreateDataSet(c) + assert.Error(t, err) + + // Echo returns an HTTPError + var he *echo.HTTPError + ok := errors.As(err, &he) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, he.Code) +} + +func TestSignCreateDataSet_InvalidDataSetId(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.CreateDataSetRequest{ + ClientDataSetId: "not-a-number", + Payee: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", + Metadata: []eip712.MetadataEntry{}, + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/create-dataset", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignCreateDataSet(c) + assert.Error(t, err) + + var he *echo.HTTPError + ok := errors.As(err, &he) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, he.Code) +} + +func TestSignCreateDataSet_InvalidPayeeAddress(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.CreateDataSetRequest{ + ClientDataSetId: "123", + Payee: "not-an-address", + Metadata: []eip712.MetadataEntry{}, + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/create-dataset", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignCreateDataSet(c) + assert.Error(t, err) + + var he *echo.HTTPError + ok := errors.As(err, &he) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, he.Code) +} + +func TestSignAddPieces_Success(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.AddPiecesRequest{ + ClientDataSetId: "123", + Nonce: "1", + PieceData: []string{ + "0x0001020304", + "0x0506070809", + }, + Metadata: [][]eip712.MetadataEntry{ + {{Key: "size", Value: "1024"}}, + {{Key: "size", Value: "2048"}}, + }, + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/add-pieces", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignAddPieces(c) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, rec.Code) + + var signature eip712.AuthSignature + err = json.Unmarshal(rec.Body.Bytes(), &signature) + require.NoError(t, err) + + assert.NotEmpty(t, signature.Signature) + assert.Equal(t, s.GetAddress(), signature.Signer) +} + +func TestSignAddPieces_InvalidDataSetId(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.AddPiecesRequest{ + ClientDataSetId: "invalid", + Nonce: "0", + PieceData: []string{}, + Metadata: [][]eip712.MetadataEntry{}, + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/add-pieces", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignAddPieces(c) + assert.Error(t, err) + + var he *echo.HTTPError + ok := errors.As(err, &he) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, he.Code) +} + +func TestSignAddPieces_InvalidNonce(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.AddPiecesRequest{ + ClientDataSetId: "123", + Nonce: "not-a-number", + PieceData: []string{}, + Metadata: [][]eip712.MetadataEntry{}, + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/add-pieces", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignAddPieces(c) + assert.Error(t, err) + + var he *echo.HTTPError + ok := errors.As(err, &he) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, he.Code) +} + +func TestSignSchedulePieceRemovals_Success(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.SchedulePieceRemovalsRequest{ + ClientDataSetId: "123", + PieceIds: []string{"1", "2", "3"}, + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/schedule-piece-removals", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignSchedulePieceRemovals(c) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, rec.Code) + + var signature eip712.AuthSignature + err = json.Unmarshal(rec.Body.Bytes(), &signature) + require.NoError(t, err) + + assert.NotEmpty(t, signature.Signature) + assert.Equal(t, s.GetAddress(), signature.Signer) +} + +func TestSignSchedulePieceRemovals_InvalidDataSetId(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.SchedulePieceRemovalsRequest{ + ClientDataSetId: "invalid", + PieceIds: []string{}, + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/schedule-piece-removals", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignSchedulePieceRemovals(c) + assert.Error(t, err) + + var he *echo.HTTPError + ok := errors.As(err, &he) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, he.Code) +} + +func TestSignSchedulePieceRemovals_InvalidPieceId(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.SchedulePieceRemovalsRequest{ + ClientDataSetId: "123", + PieceIds: []string{"1", "invalid", "3"}, + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/schedule-piece-removals", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignSchedulePieceRemovals(c) + assert.Error(t, err) + + var he *echo.HTTPError + ok := errors.As(err, &he) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, he.Code) +} + +func TestSignDeleteDataSet_Success(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.DeleteDataSetRequest{ + ClientDataSetId: "123", + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/delete-dataset", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignDeleteDataSet(c) + require.NoError(t, err) + + assert.Equal(t, http.StatusOK, rec.Code) + + var signature eip712.AuthSignature + err = json.Unmarshal(rec.Body.Bytes(), &signature) + require.NoError(t, err) + + assert.NotEmpty(t, signature.Signature) + assert.Equal(t, s.GetAddress(), signature.Signer) +} + +func TestSignDeleteDataSet_InvalidDataSetId(t *testing.T) { + e := echo.New() + s := createTestSigner(t) + handler := NewHandler(s) + + requestBody := types.DeleteDataSetRequest{ + ClientDataSetId: "not-a-number", + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/sign/delete-dataset", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + err = handler.SignDeleteDataSet(c) + assert.Error(t, err) + + var he *echo.HTTPError + ok := errors.As(err, &he) + assert.True(t, ok) + assert.Equal(t, http.StatusBadRequest, he.Code) +} + +// TestSignatureConsistency verifies that signing the same data twice produces the same result +func TestSignatureConsistency(t *testing.T) { + e := echo.New() + + // Create signer with fixed key for reproducibility + privateKey, err := crypto.HexToECDSA("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + require.NoError(t, err) + + chainID := big.NewInt(314159) + contractAddr := common.HexToAddress("0x8b7aa0a68f5717e400F1C4D37F7a28f84f76dF91") + s := signer.NewSigner(privateKey, chainID, contractAddr) + handler := NewHandler(s) + + requestBody := types.DeleteDataSetRequest{ + ClientDataSetId: "123", + } + + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + // Sign first time + req1 := httptest.NewRequest(http.MethodPost, "/sign/delete-dataset", strings.NewReader(string(body))) + req1.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec1 := httptest.NewRecorder() + c1 := e.NewContext(req1, rec1) + + err = handler.SignDeleteDataSet(c1) + require.NoError(t, err) + + var sig1 eip712.AuthSignature + err = json.Unmarshal(rec1.Body.Bytes(), &sig1) + require.NoError(t, err) + + // Sign second time + req2 := httptest.NewRequest(http.MethodPost, "/sign/delete-dataset", strings.NewReader(string(body))) + req2.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec2 := httptest.NewRecorder() + c2 := e.NewContext(req2, rec2) + + err = handler.SignDeleteDataSet(c2) + require.NoError(t, err) + + var sig2 eip712.AuthSignature + err = json.Unmarshal(rec2.Body.Bytes(), &sig2) + require.NoError(t, err) + + // Signatures should be identical for the same data + assert.Equal(t, sig1.Signature, sig2.Signature) + assert.Equal(t, sig1.V, sig2.V) + assert.Equal(t, sig1.R, sig2.R) + assert.Equal(t, sig1.S, sig2.S) + assert.Equal(t, sig1.SignedData, sig2.SignedData) +} + +// BenchmarkSignCreateDataSet benchmarks the CreateDataSet signing endpoint +func BenchmarkSignCreateDataSet(b *testing.B) { + e := echo.New() + s := createTestSigner(&testing.T{}) + handler := NewHandler(s) + + requestBody := types.CreateDataSetRequest{ + ClientDataSetId: "123", + Payee: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", + Metadata: []eip712.MetadataEntry{ + {Key: "name", Value: "benchmark-dataset"}, + }, + } + + body, _ := json.Marshal(requestBody) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + req := httptest.NewRequest(http.MethodPost, "/sign/create-dataset", strings.NewReader(string(body))) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + require.NoError(b, handler.SignCreateDataSet(c)) + } +} diff --git a/piri-signing-service/pkg/server/handlers/access_grant.go b/piri-signing-service/pkg/server/handlers/access_grant.go new file mode 100644 index 00000000..d6712ec4 --- /dev/null +++ b/piri-signing-service/pkg/server/handlers/access_grant.go @@ -0,0 +1,74 @@ +package handlers + +import ( + "strings" + "time" + + "github.com/fil-forge/forge/protocol/commands/access" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/fil-forge/ucantone/ucan/delegation" + "github.com/ipfs/go-cid" +) + +// validity is the time a granted delegation is valid for. +const grantValidity = time.Hour + +// NewAccessGrantHandler returns a bindexec UCAN handler for /access/grant. +// It issues a one-hour delegation per requested capability, returning the +// CIDs in the receipt OK and attaching the signed delegations to the +// response container as metadata. +func NewAccessGrantHandler(id ucan.Issuer) binding.HandlerFunc[*access.GrantArguments, *access.GrantOK] { + return func(req *binding.Request[*access.GrantArguments], res *binding.Response[*access.GrantOK]) error { + args := req.Task().Arguments() + inv := req.Invocation() + + log.Infow( + "handling access grant", + "command", access.GrantCommand, + "issuer", inv.Issuer(), + "attenuations", args.Attenuations, + "cause", args.Cause, + ) + + if len(args.Attenuations) == 0 { + return res.SetFailure(access.ErrMissingCapability) + } + + audience := inv.Issuer() + exp := ucan.UnixTimestamp(time.Now().Add(grantValidity).Unix()) + + delegations := make([]ucan.Delegation, 0, len(args.Attenuations)) + links := make([]cid.Cid, 0, len(args.Attenuations)) + for _, ar := range args.Attenuations { + if !strings.HasPrefix(ar.Command.String(), "/pdp/sign/") { + return res.SetFailure(errors.New(access.UnknownAbilityErrorName, + "unknown ability: %s", ar.Command)) + } + + dlg, err := delegation.Delegate( + id, + audience, + id.DID(), + ar.Command, + delegation.WithExpiration(exp), + ) + if err != nil { + return res.SetFailure(err) + } + log.Infow("delegated capability", "command", ar.Command, "audience", audience) + delegations = append(delegations, dlg) + links = append(links, dlg.Link()) + } + + // Attach the signed delegation envelopes as response metadata so the + // caller can recover them from the receipt container. + if err := res.SetMetadata(container.New(container.WithDelegations(delegations...))); err != nil { + return err + } + + return res.SetSuccess(&access.GrantOK{Delegations: links}) + } +} diff --git a/piri-signing-service/pkg/server/handlers/access_grant_test.go b/piri-signing-service/pkg/server/handlers/access_grant_test.go new file mode 100644 index 00000000..2a1585ac --- /dev/null +++ b/piri-signing-service/pkg/server/handlers/access_grant_test.go @@ -0,0 +1,69 @@ +package handlers_test + +import ( + "bytes" + "testing" + + "github.com/fil-forge/forge/protocol/commands/access" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/testutil" + "github.com/fil-forge/ucantone/ucan/command" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/stretchr/testify/require" + + "github.com/fil-forge/forge/piri-signing-service/pkg/server/handlers" +) + +func TestAccessGrant_Success(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + handler := handlers.NewAccessGrantHandler(service) + + inv, err := access.Grant.Invoke(alice, service.DID(), &access.GrantArguments{ + Attenuations: []access.CapabilityRequest{{Command: command.MustParse("/pdp/sign/pieces/add")}}, + }) + require.NoError(t, err) + + req, err := binding.NewRequest[*access.GrantArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*access.GrantOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + + okBytes, errBytes := res.Receipt().Out().Unpack() + require.Nil(t, errBytes) + require.NotNil(t, okBytes) + + var ok access.GrantOK + require.NoError(t, ok.UnmarshalCBOR(bytes.NewReader(okBytes))) + require.Len(t, ok.Delegations, 1) + + // Delegation envelope is attached to the response metadata. + require.Len(t, res.Metadata().Delegations(), 1) +} + +func TestAccessGrant_UnknownAbility(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + handler := handlers.NewAccessGrantHandler(service) + + inv, err := access.Grant.Invoke(alice, service.DID(), &access.GrantArguments{ + Attenuations: []access.CapabilityRequest{{Command: command.MustParse("/foo/bar")}}, + }) + require.NoError(t, err) + + req, err := binding.NewRequest[*access.GrantArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*access.GrantOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + + require.False(t, res.Receipt().Out().IsOK()) +} + +// silence unused-import warning while keeping a hook for future test helpers +var _ = invocation.Invoke diff --git a/piri-signing-service/pkg/server/handlers/sign.go b/piri-signing-service/pkg/server/handlers/sign.go new file mode 100644 index 00000000..335d267d --- /dev/null +++ b/piri-signing-service/pkg/server/handlers/sign.go @@ -0,0 +1,163 @@ +package handlers + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/fil-forge/filecoin-services/go/eip712" + "github.com/fil-forge/forge/protocol/commands/pdp/sign" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/ucan" + logging "github.com/ipfs/go-log/v2" + + "github.com/fil-forge/forge/internal/pdpsigner/types" +) + +var log = logging.Logger("pkg/server/handlers") + +// InvalidResourceErrorName is the receipt failure name returned when an +// invocation's subject does not match the service DID. +const InvalidResourceErrorName = "InvalidResource" + +func newInvalidResourceError(expected, actual string) error { + return errors.New(InvalidResourceErrorName, + "invalid resource, expected: %s got: %s", expected, actual) +} + +// NewDataSetCreateHandler wraps the underlying eip712 signer in a +// binding UCAN handler for /pdp/sign/dataset/create. +func NewDataSetCreateHandler(id ucan.Issuer, signer types.OperationSigner) binding.HandlerFunc[*sign.DataSetCreateArguments, *sign.DataSetCreateOK] { + return func(req *binding.Request[*sign.DataSetCreateArguments], res *binding.Response[*sign.DataSetCreateOK]) error { + args := req.Task().Arguments() + inv := req.Invocation() + log.Infow( + "handling signing request", + "command", sign.DataSetCreate.Command, + "issuer", inv.Issuer(), + "dataset", args.DataSet.String(), + "payee", args.Payee, + "metadata", args.Metadata.Values, + ) + // issuer must have a delegation to use the service (cannot be self-signed) + if subj := req.Task().Subject().String(); subj != id.DID().String() { + return res.SetFailure(newInvalidResourceError(id.DID().String(), subj)) + } + + s, err := signer.SignCreateDataSet(args.DataSet, common.BytesToAddress(args.Payee), toEIP712MetadataEntries(args.Metadata)) + if err != nil { + return fmt.Errorf("signing create dataset: %w", err) + } + ok := toLibforgeAuthSig(s) + return res.SetSuccess(&ok) + } +} + +// NewDataSetDeleteHandler wraps the underlying eip712 signer in a +// binding UCAN handler for /pdp/sign/dataset/delete. +func NewDataSetDeleteHandler(id ucan.Issuer, signer types.OperationSigner) binding.HandlerFunc[*sign.DataSetDeleteArguments, *sign.DataSetDeleteOK] { + return func(req *binding.Request[*sign.DataSetDeleteArguments], res *binding.Response[*sign.DataSetDeleteOK]) error { + args := req.Task().Arguments() + inv := req.Invocation() + log.Infow( + "handling signing request", + "command", sign.DataSetDelete.Command, + "issuer", inv.Issuer(), + "dataset", args.DataSet.String(), + ) + if subj := req.Task().Subject().String(); subj != id.DID().String() { + return res.SetFailure(newInvalidResourceError(id.DID().String(), subj)) + } + + s, err := signer.SignDeleteDataSet(args.DataSet) + if err != nil { + return fmt.Errorf("signing delete dataset: %w", err) + } + ok := toLibforgeAuthSig(s) + return res.SetSuccess(&ok) + } +} + +// NewPiecesAddHandler wraps the underlying eip712 signer in a +// binding UCAN handler for /pdp/sign/pieces/add. +func NewPiecesAddHandler(id ucan.Issuer, signer types.OperationSigner) binding.HandlerFunc[*sign.PiecesAddArguments, *sign.PiecesAddOK] { + return func(req *binding.Request[*sign.PiecesAddArguments], res *binding.Response[*sign.PiecesAddOK]) error { + args := req.Task().Arguments() + inv := req.Invocation() + log.Infow( + "handling signing request", + "command", sign.PiecesAdd.Command, + "issuer", inv.Issuer(), + "dataset", args.DataSet.String(), + "nonce", args.Nonce.String(), + "pieces", len(args.PieceData), + ) + if subj := req.Task().Subject().String(); subj != id.DID().String() { + return res.SetFailure(newInvalidResourceError(id.DID().String(), subj)) + } + + // TODO: validate pieces against the proof container + + metadata := make([][]eip712.MetadataEntry, 0, len(args.Metadata)) + for _, m := range args.Metadata { + metadata = append(metadata, toEIP712MetadataEntries(m)) + } + + s, err := signer.SignAddPieces(args.DataSet, args.Nonce, args.PieceData, metadata) + if err != nil { + return fmt.Errorf("signing add pieces: %w", err) + } + ok := toLibforgeAuthSig(s) + return res.SetSuccess(&ok) + } +} + +// NewPiecesRemoveScheduleHandler wraps the underlying eip712 signer in a +// binding UCAN handler for /pdp/sign/pieces/remove/schedule. +func NewPiecesRemoveScheduleHandler(id ucan.Issuer, signer types.OperationSigner) binding.HandlerFunc[*sign.PiecesRemoveScheduleArguments, *sign.PiecesRemoveScheduleOK] { + return func(req *binding.Request[*sign.PiecesRemoveScheduleArguments], + res *binding.Response[*sign.PiecesRemoveScheduleOK]) error { + args := req.Task().Arguments() + inv := req.Invocation() + log.Infow( + "handling signing request", + "command", sign.PiecesRemoveSchedule.Command, + "issuer", inv.Issuer(), + "dataset", args.DataSet.String(), + "pieces", len(args.Pieces), + ) + if subj := req.Task().Subject().String(); subj != id.DID().String() { + return res.SetFailure(newInvalidResourceError(id.DID().String(), subj)) + } + + s, err := signer.SignSchedulePieceRemovals(args.DataSet, args.Pieces) + if err != nil { + return fmt.Errorf("signing schedule remove pieces: %w", err) + } + ok := toLibforgeAuthSig(s) + return res.SetSuccess(&ok) + } +} + +// toLibforgeAuthSig copies an eip712.AuthSignature into the protocol +// pdp/sign.AuthSignature wire shape (fixed-size address/hash fields become +// raw []byte). +func toLibforgeAuthSig(s *eip712.AuthSignature) sign.AuthSignature { + return sign.AuthSignature{ + Signature: s.Signature, + V: s.V, + R: s.R[:], + S: s.S[:], + SignedData: s.SignedData, + Signer: s.Signer.Bytes(), + } +} + +func toEIP712MetadataEntries(m sign.Metadata) []eip712.MetadataEntry { + meta := make([]eip712.MetadataEntry, 0, len(m.Values)) + for _, k := range m.Keys { + v := m.Values[k] + meta = append(meta, eip712.MetadataEntry{Key: k, Value: v}) + } + return meta +} diff --git a/piri-signing-service/pkg/server/handlers/sign_test.go b/piri-signing-service/pkg/server/handlers/sign_test.go new file mode 100644 index 00000000..c9bd109a --- /dev/null +++ b/piri-signing-service/pkg/server/handlers/sign_test.go @@ -0,0 +1,267 @@ +package handlers_test + +import ( + "bytes" + "encoding/hex" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/fil-forge/forge/protocol/commands/pdp/sign" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/fil-forge/forge/internal/pdpsigner/signer" + "github.com/fil-forge/forge/piri-signing-service/pkg/server/handlers" +) + +// createTestSigner creates a test eip712 signer with a random key. +func createTestSigner(t *testing.T) *signer.Signer { + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + + chainID := big.NewInt(314159) // Calibration testnet + contractAddr := common.HexToAddress("0x8b7aa0a68f5717e400F1C4D37F7a28f84f76dF91") + + return signer.NewSigner(privateKey, chainID, contractAddr) +} + +func TestSignCreateDataSet_Success(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + s := createTestSigner(t) + handler := handlers.NewDataSetCreateHandler(service, s) + + testPayee := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb") + + args := &sign.DataSetCreateArguments{ + DataSet: big.NewInt(123), + Payee: testPayee.Bytes(), + Metadata: sign.Metadata{ + Keys: []string{"name", "version"}, + Values: map[string]string{"name": "test-dataset", "version": "1.0"}, + }, + } + inv, err := sign.DataSetCreate.Invoke(alice, service.DID(), args) + require.NoError(t, err) + + req, err := binding.NewRequest[*sign.DataSetCreateArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*sign.DataSetCreateOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + okBytes, errBytes := res.Receipt().Out().Unpack() + require.Nil(t, errBytes) + require.NotNil(t, okBytes) + + var got sign.AuthSignature + require.NoError(t, got.UnmarshalCBOR(bytes.NewReader(okBytes))) + assertSignature(t, s.GetAddress(), got) +} + +func TestSignCreateDataSet_InvalidResource(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + s := createTestSigner(t) + handler := handlers.NewDataSetCreateHandler(service, s) + + testPayee := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb") + + args := &sign.DataSetCreateArguments{ + DataSet: big.NewInt(123), + Payee: testPayee.Bytes(), + Metadata: sign.Metadata{ + Keys: []string{"name", "version"}, + Values: map[string]string{"name": "test-dataset", "version": "1.0"}, + }, + } + // invoke with alice's DID as the subject (instead of the service DID) + inv, err := sign.DataSetCreate.Invoke(alice, alice.DID(), args) + require.NoError(t, err) + + req, err := binding.NewRequest[*sign.DataSetCreateArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*sign.DataSetCreateOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + require.False(t, res.Receipt().Out().IsOK()) +} + +func TestSignDeleteDataSet_Success(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + s := createTestSigner(t) + handler := handlers.NewDataSetDeleteHandler(service, s) + + args := &sign.DataSetDeleteArguments{DataSet: big.NewInt(123)} + inv, err := sign.DataSetDelete.Invoke(alice, service.DID(), args) + require.NoError(t, err) + + req, err := binding.NewRequest[*sign.DataSetDeleteArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*sign.DataSetDeleteOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + okBytes, errBytes := res.Receipt().Out().Unpack() + require.Nil(t, errBytes) + require.NotNil(t, okBytes) + + var got sign.AuthSignature + require.NoError(t, got.UnmarshalCBOR(bytes.NewReader(okBytes))) + assertSignature(t, s.GetAddress(), got) +} + +func TestSignDeleteDataSet_InvalidResource(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + s := createTestSigner(t) + handler := handlers.NewDataSetDeleteHandler(service, s) + + args := &sign.DataSetDeleteArguments{DataSet: big.NewInt(123)} + inv, err := sign.DataSetDelete.Invoke(alice, alice.DID(), args) + require.NoError(t, err) + + req, err := binding.NewRequest[*sign.DataSetDeleteArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*sign.DataSetDeleteOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + require.False(t, res.Receipt().Out().IsOK()) +} + +func TestSignAddPieces_Success(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + s := createTestSigner(t) + handler := handlers.NewPiecesAddHandler(service, s) + + args := &sign.PiecesAddArguments{ + DataSet: big.NewInt(123), + Nonce: big.NewInt(0), + PieceData: [][]byte{ + mustHex(t, "0001020304"), + mustHex(t, "0506070809"), + }, + Metadata: []sign.Metadata{ + {Keys: []string{"size"}, Values: map[string]string{"size": "1024"}}, + {Keys: []string{"size"}, Values: map[string]string{"size": "2048"}}, + }, + } + inv, err := sign.PiecesAdd.Invoke(alice, service.DID(), args) + require.NoError(t, err) + + req, err := binding.NewRequest[*sign.PiecesAddArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*sign.PiecesAddOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + okBytes, errBytes := res.Receipt().Out().Unpack() + require.Nil(t, errBytes) + require.NotNil(t, okBytes) + + var got sign.AuthSignature + require.NoError(t, got.UnmarshalCBOR(bytes.NewReader(okBytes))) + assertSignature(t, s.GetAddress(), got) +} + +func TestSignAddPieces_InvalidResource(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + s := createTestSigner(t) + handler := handlers.NewPiecesAddHandler(service, s) + + args := &sign.PiecesAddArguments{ + DataSet: big.NewInt(123), + Nonce: big.NewInt(0), + } + inv, err := sign.PiecesAdd.Invoke(alice, alice.DID(), args) + require.NoError(t, err) + + req, err := binding.NewRequest[*sign.PiecesAddArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*sign.PiecesAddOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + require.False(t, res.Receipt().Out().IsOK()) +} + +func TestSignScheduleRemovePieces_Success(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + s := createTestSigner(t) + handler := handlers.NewPiecesRemoveScheduleHandler(service, s) + + args := &sign.PiecesRemoveScheduleArguments{ + DataSet: big.NewInt(123), + Pieces: []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)}, + } + inv, err := sign.PiecesRemoveSchedule.Invoke(alice, service.DID(), args) + require.NoError(t, err) + + req, err := binding.NewRequest[*sign.PiecesRemoveScheduleArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*sign.PiecesRemoveScheduleOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + okBytes, errBytes := res.Receipt().Out().Unpack() + require.Nil(t, errBytes) + require.NotNil(t, okBytes) + + var got sign.AuthSignature + require.NoError(t, got.UnmarshalCBOR(bytes.NewReader(okBytes))) + assertSignature(t, s.GetAddress(), got) +} + +func TestSignScheduleRemovePieces_InvalidResource(t *testing.T) { + alice := testutil.RandomIssuer(t) + service := testutil.RandomIssuer(t) + + s := createTestSigner(t) + handler := handlers.NewPiecesRemoveScheduleHandler(service, s) + + args := &sign.PiecesRemoveScheduleArguments{ + DataSet: big.NewInt(123), + Pieces: []*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)}, + } + inv, err := sign.PiecesRemoveSchedule.Invoke(alice, alice.DID(), args) + require.NoError(t, err) + + req, err := binding.NewRequest[*sign.PiecesRemoveScheduleArguments](t.Context(), inv) + require.NoError(t, err) + res, err := binding.NewResponse(inv.Task().Link(), binding.WithIssuer[*sign.PiecesRemoveScheduleOK](service)) + require.NoError(t, err) + + require.NoError(t, handler(req, res)) + require.False(t, res.Receipt().Out().IsOK()) +} + +func assertSignature(t *testing.T, signerAddr common.Address, sig sign.AuthSignature) { + assert.NotEmpty(t, sig.Signature) + require.Len(t, sig.Signer, len(common.Address{})) + assert.Equal(t, signerAddr, common.BytesToAddress(sig.Signer)) + assert.NotEmpty(t, sig.SignedData) + assert.True(t, sig.V == 27 || sig.V == 28, "V should be 27 or 28") +} + +func mustHex(t *testing.T, s string) []byte { + b, err := hex.DecodeString(s) + require.NoError(t, err) + return b +} diff --git a/piri-signing-service/pkg/server/server.go b/piri-signing-service/pkg/server/server.go new file mode 100644 index 00000000..e08de6b3 --- /dev/null +++ b/piri-signing-service/pkg/server/server.go @@ -0,0 +1,90 @@ +package server + +import ( + "fmt" + "time" + + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/access" + "github.com/fil-forge/forge/protocol/commands/pdp/sign" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/did/key" + "github.com/fil-forge/ucantone/did/resolver" + "github.com/fil-forge/ucantone/did/web" + "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/validator" + + "github.com/fil-forge/forge/internal/pdpsigner/types" + "github.com/fil-forge/forge/piri-signing-service/pkg/server/handlers" +) + +type config struct { + insecureDidResolution bool +} + +type Option func(*config) error + +func WithInsecureDIDResolution() Option { + return func(c *config) error { + c.insecureDidResolution = true + return nil + } +} + +// New constructs a UCAN 1.0 HTTP server that handles the five +// signing-service capabilities: +// +// - /access/grant — issue a delegation for a /pdp/sign/* ability +// - /pdp/sign/dataset/create +// - /pdp/sign/dataset/delete +// - /pdp/sign/pieces/add +// - /pdp/sign/pieces/remove/schedule +// +// The returned server implements http.Handler. +// +// Piri clients sign invocations as did:web:piri-N, so the server registers a +// did:web resolver (HTTPS did.json fetch, cached; HTTP only with the insecure +// dev flag). Without this, the default validator only accepts did:key issuers +// and rejects invocations with "unsupported DID method: web". Returns an error +// if the resolver fails to initialize. +func New(id identity.Identity, signer types.OperationSigner, opts ...Option) (*server.HTTPServer, error) { + cfg := &config{} + for _, opt := range opts { + if err := opt(cfg); err != nil { + return nil, err + } + } + webResolverOpts := []web.Option{} + if cfg.insecureDidResolution { + webResolverOpts = append(webResolverOpts, web.WithInsecure(true)) + } + webResolver, err := web.NewResolver(webResolverOpts...) + if err != nil { + return nil, err + } + + selfDoc, err := identity.Identity{Issuer: id}.DIDDocument() + if err != nil { + return nil, fmt.Errorf("creating DID document for service identity: %w", err) + } + + srv := server.NewHTTP(id, + server.WithValidationOptions( + validator.WithDIDResolver(resolver.ByMethod{ + "key": key.Resolver, + "web": resolver.Tiered{ + resolver.WellKnown{id.DID(): selfDoc}, + resolver.NewCached(webResolver, time.Hour*3), + }, + }), + ), + ) + + srv.Handle(access.Grant.Command, binding.NewHandler(handlers.NewAccessGrantHandler(id))) + srv.Handle(sign.DataSetCreate.Command, binding.NewHandler(handlers.NewDataSetCreateHandler(id, signer))) + srv.Handle(sign.DataSetDelete.Command, binding.NewHandler(handlers.NewDataSetDeleteHandler(id, signer))) + srv.Handle(sign.PiecesAdd.Command, binding.NewHandler(handlers.NewPiecesAddHandler(id, signer))) + srv.Handle(sign.PiecesRemoveSchedule.Command, binding.NewHandler(handlers.NewPiecesRemoveScheduleHandler(id, signer))) + + return srv, nil +} diff --git a/piri-signing-service/signer.yaml.example b/piri-signing-service/signer.yaml.example new file mode 100644 index 00000000..5f847fa5 --- /dev/null +++ b/piri-signing-service/signer.yaml.example @@ -0,0 +1,49 @@ +# Piri Signing Service Configuration + +# Network binding configuration +host: localhost # Host to bind the HTTP server (default: localhost) +port: 7446 # Port to bind the HTTP server (default: 7446, spells SIGN on T9 :) ) + +# Ethereum RPC endpoint +# Required: Must be a valid Ethereum RPC URL +# Example for Filecoin Calibration testnet: https://api.calibration.node.glif.io/rpc/v1 +# Example for Filecoin Mainnet: https://api.node.glif.io/rpc/v1 +rpc_url: https://api.calibration.node.glif.io/rpc/v1 + +# FilecoinWarmStorageService contract address +# Required: The deployed contract address for signature verification +# This address must match the on-chain deployment +service_contract_address: "0x0000000000000000000000000000000000000000" + +# Signing key configuration (choose one method) +# Method 1: Hex-encoded private signing key +signing_key: "0x0000000000000000000000000000000000000000" + +# Method 2: Direct signing key file path +# signing_key_path: /path/to/your/signing-key.hex + +# Method 3: Encrypted keystore file +# signing_keystore_path: /path/to/your/signing-keystore.json +# signing_keystore_password: your-signing-keystore-password + +# Environment variable overrides: +# You can override any of these settings using environment variables: +# SIGNING_SERVICE_HOST=0.0.0.0 +# SIGNING_SERVICE_PORT=8080 +# SIGNING_SERVICE_RPC_URL=https://your-rpc-endpoint.com +# SIGNING_SERVICE_SERVICE_CONTRACT_ADDRESS=0xYourContractAddress +# SIGNING_SERVICE_SIGNING_KEY=0xYourSigningKey +# SIGNING_SERVICE_SIGNING_KEY_PATH=/secure/path/to/signing-key.hex +# SIGNING_SERVICE_SIGNING_KEYSTORE_PATH=/secure/path/to/signing-keystore.json +# SIGNING_SERVICE_SIGNING_KEYSTORE_PASSWORD=your-signing-keystore-password + +# Command-line flag overrides: +# You can override any setting via command-line flags: +# --host=0.0.0.0 +# --port=8080 +# --rpc-url=https://your-rpc-endpoint.com +# --contract-address=0xYourContractAddress +# --signing-key=0xYourSigningKey +# --signing-key-path=/secure/path/to/signing-key.hex +# --signing-keystore-path=/secure/path/to/signing-keystore.json +# --signing-keystore-password=your-signing-keystore-password \ No newline at end of file diff --git a/piri-signing-service/version.json b/piri-signing-service/version.json new file mode 100644 index 00000000..14ed2238 --- /dev/null +++ b/piri-signing-service/version.json @@ -0,0 +1,3 @@ +{ + "version": "v0.1.0" +} diff --git a/piri/AGENTS.md b/piri/AGENTS.md index 7cdb0734..ddb305ca 100644 --- a/piri/AGENTS.md +++ b/piri/AGENTS.md @@ -64,8 +64,8 @@ docs/ # mkdocs operator documentation (content/ + mkdocs.yml) ## UCAN stack Piri uses `github.com/fil-forge/ucantone` (UCAN server/execution runtime) and -`github.com/fil-forge/libforge` (shared command/capability definitions under -`libforge/commands/...`, identity, retrieval protocol). There are two UCAN +`github.com/fil-forge/forge/protocol` (shared command/capability definitions under +`protocol/commands/...`, identity, retrieval protocol). There are two UCAN servers with distinct response shapes: - **RPC server** (`server.NewHTTP`, body-CAR): invocations whose response is a @@ -81,7 +81,7 @@ Handlers register through fx group tags. Each handler package has a server options). The group-tag strings live in `pkg/ucanhandlers/handler.go` and appear in both the struct tags and helpers — keep them in sync. -To add a capability handler: define/import the command from libforge, write +To add a capability handler: define/import the command from the in-repo `protocol` module, write the handler in the appropriate `pkg/ucanhandlers//` package, register it in that package's `module.go`, and add coverage via the `ucanfxtest` harnesses. @@ -113,9 +113,9 @@ to cover the new graph shape. - `blob/accept` kicks off the whole PDP pipeline — CommP calculation, piece aggregation into proof sets, and on-chain interaction. Changes there cascade through `pkg/pdp/`. -- Capability/command schemas come from `libforge`; they must stay +- Capability/command schemas come from the in-repo `protocol` module; they must stay wire-compatible with the other services that speak these protocols. Don't - change invocation/receipt shapes unilaterally — coordinate with libforge and + change invocation/receipt shapes unilaterally — coordinate with `protocol` and the consuming services. - Retrieval handler changes affect live client content access. - Config changes frequently change the fx graph — see the dependency graph diff --git a/piri/Makefile b/piri/Makefile index bde9ea40..98367290 100644 --- a/piri/Makefile +++ b/piri/Makefile @@ -28,7 +28,7 @@ export GOWORK := off # direct exec of a bare 'docker'. DOCKER := $(shell command -v docker) -# Container image via the monorepo-shared docker/Dockerfile (context = this dir). +# Container image via the monorepo-shared docker/Dockerfile (context = the repo root). IMAGE_BUILD_ARGS := --build-arg SERVICE=piri --build-arg BUILD_TAGS=skiff .PHONY: all build test vet image image-dev clean @@ -54,10 +54,10 @@ vet: CGO_ENABLED=0 go vet $(TAGS) ./... image: - $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/piri:local . + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/piri:local .. image-dev: - $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/piri:local-dev . + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/piri:local-dev .. clean: rm -f ./piri \ No newline at end of file diff --git a/piri/cmd/cli/identity/root.go b/piri/cmd/cli/identity/root.go index 75a9414b..103d3b19 100644 --- a/piri/cmd/cli/identity/root.go +++ b/piri/cmd/cli/identity/root.go @@ -5,7 +5,7 @@ import ( "io" "os" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/spf13/cobra" ) diff --git a/piri/cmd/cli/setup/register.go b/piri/cmd/cli/setup/register.go index 4f9511ca..c0eba23e 100644 --- a/piri/cmd/cli/setup/register.go +++ b/piri/cmd/cli/setup/register.go @@ -14,9 +14,9 @@ import ( "github.com/BurntSushi/toml" "github.com/ethereum/go-ethereum/common" - "github.com/fil-forge/libforge/commands/blob" - replicacmds "github.com/fil-forge/libforge/commands/blob/replica" - "github.com/fil-forge/libforge/commands/pdp" + "github.com/fil-forge/forge/protocol/commands/blob" + replicacmds "github.com/fil-forge/forge/protocol/commands/blob/replica" + "github.com/fil-forge/forge/protocol/commands/pdp" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" @@ -33,7 +33,7 @@ import ( "github.com/fil-forge/forge/piri/pkg/store/local/keystore" "github.com/fil-forge/forge/piri/pkg/wallet" - delgclient "github.com/fil-forge/delegator/client" + delgclient "github.com/fil-forge/forge/internal/client/delegator" "github.com/fil-forge/forge/piri/pkg/config" appcfg "github.com/fil-forge/forge/piri/pkg/config/app" diff --git a/piri/go.mod b/piri/go.mod index aa8ff19b..83b20d20 100644 --- a/piri/go.mod +++ b/piri/go.mod @@ -9,12 +9,11 @@ require ( github.com/deckarep/golang-set/v2 v2.9.0 github.com/docker/docker v28.5.2+incompatible github.com/ethereum/go-ethereum v1.17.5 - github.com/fil-forge/delegator v0.0.0-20260814010356-6b1794dbbab9 github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 + github.com/fil-forge/forge/internal v0.0.0 + github.com/fil-forge/forge/protocol v0.0.0 github.com/fil-forge/go-ipni-tools v0.0.0-20260820150418-78ca0c16834d - github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5 - github.com/fil-forge/piri-signing-service v0.0.0-20260801143551-d7c06ad1d899 - github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 github.com/filecoin-project/curio v1.28.3-0.20260717015646-38ca280c43a2 github.com/filecoin-project/go-commp-utils/nonffi v0.0.0-20240802040721-2a04ffc8ffe8 github.com/filecoin-project/go-commp-utils/v2 v2.1.0 @@ -311,7 +310,7 @@ require ( go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect golang.org/x/crypto v0.55.0 // indirect - golang.org/x/exp v0.0.0-20260718201538-764159d718ef // indirect + golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect golang.org/x/net v0.58.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/term v0.45.0 // indirect @@ -333,3 +332,7 @@ require ( ) replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20240617180043-68d350f18fd4 + +replace github.com/fil-forge/forge/protocol => ../protocol + +replace github.com/fil-forge/forge/internal => ../internal diff --git a/piri/go.sum b/piri/go.sum index f6e8d1de..5b930e3e 100644 --- a/piri/go.sum +++ b/piri/go.sum @@ -544,18 +544,14 @@ github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeO github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= -github.com/fil-forge/delegator v0.0.0-20260814010356-6b1794dbbab9 h1:3fJu3pXa3WBAjfWpFynu0o+Av8FQeELrd1yW58hMdFI= -github.com/fil-forge/delegator v0.0.0-20260814010356-6b1794dbbab9/go.mod h1:ujU9zx+XbebCDsRJqO/2E1qV+c6RIybXbHLnQiCrRNM= github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 h1:h7SZVboCO1iPDtdxPJjs1fj4IVFq/dpDWFevkXtwgps= github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390/go.mod h1:ur5fRrG8v9twwwRFdX7lquGza4liT6x/xAFW7T4XVIc= github.com/fil-forge/go-ipni-tools v0.0.0-20260820150418-78ca0c16834d h1:iEXJC5aJgRHq2iM4X6usfow436OQGauLjDuga1Hiq4o= github.com/fil-forge/go-ipni-tools v0.0.0-20260820150418-78ca0c16834d/go.mod h1:fwLjqz34DvfoWcuzyH8Wr7dTJdr3mzYjxZRweysHOH4= -github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5 h1:Pu7Xk/LdaQyv1560k1MbL6CQUdWxZJIG9elEE0vkgLY= -github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5/go.mod h1:aL/KcXZ0KVs1ymAYqIwQyCnl2p481InN+gYbRLNM8fI= -github.com/fil-forge/piri-signing-service v0.0.0-20260801143551-d7c06ad1d899 h1:5dV7QmNHHA5JZLIGikLJJsmB/sdmqzyTgr75ivInfVY= -github.com/fil-forge/piri-signing-service v0.0.0-20260801143551-d7c06ad1d899/go.mod h1:6Xzvs1sZrikN3GygCV2I96dnbzy1xuCtitPMme/TjZU= -github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1 h1:Tzy3lZ7+LAyVK4+qyWM97UMOtnXx05Wdn+rcTDLdBDE= -github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +github.com/fil-forge/libforge v0.0.0-20260701162346-f0706e1641a3 h1:/EDxpbuVeSXH3FLF7MK4G4wrDUFmCO9fLQHL23RZ2uU= +github.com/fil-forge/libforge v0.0.0-20260701162346-f0706e1641a3/go.mod h1:0kXihIQ4L2uZ00nR5XrZ/Y8Db7Ht/qQNuiWslwMJ95M= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= github.com/filecoin-project/curio v1.28.3-0.20260717015646-38ca280c43a2 h1:Abzcl/chtWOahrGeF/+q0phHl+iOZpfMNp28mr5NFEY= github.com/filecoin-project/curio v1.28.3-0.20260717015646-38ca280c43a2/go.mod h1:Sd4Ok8KKpt9JyUOh2kL0skv/ccXdEbeGT/eJ2tMJ3Uk= github.com/filecoin-project/filecoin-ffi v1.36.1 h1:SCN0ue1bn6cSIMiYkhWnDfA1fHttKBkNWJutVkB2eR4= @@ -1910,8 +1906,8 @@ 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-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= -golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= 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= diff --git a/piri/lib/mahttp_test.go b/piri/lib/mahttp_test.go index 71e550c0..8f840649 100644 --- a/piri/lib/mahttp_test.go +++ b/piri/lib/mahttp_test.go @@ -4,7 +4,7 @@ import ( "net/url" "testing" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/ipni/go-libipni/maurl" "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" diff --git a/piri/pkg/admin/httpapi/client/client.go b/piri/pkg/admin/httpapi/client/client.go index 53b7a498..e811b39c 100644 --- a/piri/pkg/admin/httpapi/client/client.go +++ b/piri/pkg/admin/httpapi/client/client.go @@ -14,7 +14,7 @@ import ( "github.com/golang-jwt/jwt/v4" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/piri/pkg/admin/httpapi" "github.com/fil-forge/forge/piri/pkg/config" "github.com/fil-forge/ucantone/multikey" diff --git a/piri/pkg/client/receipts/client_test.go b/piri/pkg/client/receipts/client_test.go index d0ffbc36..04493b78 100644 --- a/piri/pkg/client/receipts/client_test.go +++ b/piri/pkg/client/receipts/client_test.go @@ -10,8 +10,8 @@ import ( "github.com/fil-forge/go-ucanto/core/message" "github.com/fil-forge/go-ucanto/transport/car/response" - "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/fil-forge/ucantone/ucan/receipt" "github.com/stretchr/testify/require" diff --git a/piri/pkg/config/app/pdp.go b/piri/pkg/config/app/pdp.go index 9e9bd82c..144bf2c8 100644 --- a/piri/pkg/config/app/pdp.go +++ b/piri/pkg/config/app/pdp.go @@ -8,7 +8,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - signerclient "github.com/fil-forge/piri-signing-service/pkg/client" + signerclient "github.com/fil-forge/forge/internal/pdpsigner/client" "github.com/fil-forge/forge/piri/pkg/pdp/aggregation/aggregator" ) diff --git a/piri/pkg/config/identity.go b/piri/pkg/config/identity.go index ce6154b0..5ec60101 100644 --- a/piri/pkg/config/identity.go +++ b/piri/pkg/config/identity.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/piri/pkg/config/app" "github.com/fil-forge/ucantone/multikey" ) diff --git a/piri/pkg/config/pdp.go b/piri/pkg/config/pdp.go index f24b643b..12335834 100644 --- a/piri/pkg/config/pdp.go +++ b/piri/pkg/config/pdp.go @@ -12,7 +12,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - signerclient "github.com/fil-forge/piri-signing-service/pkg/client" + signerclient "github.com/fil-forge/forge/internal/pdpsigner/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/forge/piri/pkg/config/app" diff --git a/piri/pkg/fx/identity/provider.go b/piri/pkg/fx/identity/provider.go index 6e27c2be..36611823 100644 --- a/piri/pkg/fx/identity/provider.go +++ b/piri/pkg/fx/identity/provider.go @@ -3,7 +3,7 @@ package identity import ( "go.uber.org/fx" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/forge/piri/pkg/config/app" diff --git a/piri/pkg/fx/identity/provider_test.go b/piri/pkg/fx/identity/provider_test.go index 212a32f4..edd4f57b 100644 --- a/piri/pkg/fx/identity/provider_test.go +++ b/piri/pkg/fx/identity/provider_test.go @@ -3,7 +3,7 @@ package identity import ( "testing" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/multikey" "github.com/fil-forge/ucantone/multikey/ed25519" "github.com/fil-forge/ucantone/ucan" diff --git a/piri/pkg/fx/pdp/provider.go b/piri/pkg/fx/pdp/provider.go index 507e12b0..7a1b7314 100644 --- a/piri/pkg/fx/pdp/provider.go +++ b/piri/pkg/fx/pdp/provider.go @@ -6,9 +6,9 @@ import ( "github.com/fil-forge/filecoin-services/go/eip712" "go.uber.org/fx" - signerimpl "github.com/fil-forge/piri-signing-service/pkg/inprocess" - signingservice "github.com/fil-forge/piri-signing-service/pkg/signer" - signertypes "github.com/fil-forge/piri-signing-service/pkg/types" + signerimpl "github.com/fil-forge/forge/internal/pdpsigner/inprocess" + signingservice "github.com/fil-forge/forge/internal/pdpsigner/signer" + signertypes "github.com/fil-forge/forge/internal/pdpsigner/types" // curio infra "github.com/filecoin-project/curio/harmony/harmonydb" diff --git a/piri/pkg/fx/root/provider.go b/piri/pkg/fx/root/provider.go index d549957e..563afa11 100644 --- a/piri/pkg/fx/root/provider.go +++ b/piri/pkg/fx/root/provider.go @@ -4,7 +4,7 @@ import ( "github.com/labstack/echo/v4" "go.uber.org/fx" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" echofx "github.com/fil-forge/forge/piri/pkg/fx/echo" "github.com/fil-forge/forge/piri/pkg/server" ) diff --git a/piri/pkg/pdp/aggregation/aggregator/aggregate.go b/piri/pkg/pdp/aggregation/aggregator/aggregate.go index 31990577..4d3e0ac0 100644 --- a/piri/pkg/pdp/aggregation/aggregator/aggregate.go +++ b/piri/pkg/pdp/aggregation/aggregator/aggregate.go @@ -11,7 +11,7 @@ import ( "github.com/filecoin-project/go-data-segment/merkletree" logging "github.com/ipfs/go-log/v2" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" "github.com/fil-forge/forge/piri/pkg/pdp/aggregation/types" ) diff --git a/piri/pkg/pdp/aggregation/aggregator/aggregate_test.go b/piri/pkg/pdp/aggregation/aggregator/aggregate_test.go index 8816b544..e7ab2b8f 100644 --- a/piri/pkg/pdp/aggregation/aggregator/aggregate_test.go +++ b/piri/pkg/pdp/aggregation/aggregator/aggregate_test.go @@ -12,7 +12,7 @@ import ( commp "github.com/filecoin-project/go-fil-commp-hashhash" "github.com/stretchr/testify/require" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" "github.com/fil-forge/forge/piri/pkg/pdp/aggregation/aggregator" ) diff --git a/piri/pkg/pdp/aggregation/aggregator/fns_test.go b/piri/pkg/pdp/aggregation/aggregator/fns_test.go index 1b66f30e..2d78c252 100644 --- a/piri/pkg/pdp/aggregation/aggregator/fns_test.go +++ b/piri/pkg/pdp/aggregation/aggregator/fns_test.go @@ -10,7 +10,7 @@ import ( "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" "github.com/fil-forge/forge/piri/pkg/pdp/aggregation/aggregator" ) diff --git a/piri/pkg/pdp/aggregation/aggregator/fold.go b/piri/pkg/pdp/aggregation/aggregator/fold.go index 012c0c3c..f814e609 100644 --- a/piri/pkg/pdp/aggregation/aggregator/fold.go +++ b/piri/pkg/pdp/aggregation/aggregator/fold.go @@ -4,7 +4,7 @@ import ( "cmp" "slices" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" "github.com/fil-forge/forge/piri/pkg/pdp/aggregation/types" ) diff --git a/piri/pkg/pdp/aggregation/manager/accepter.go b/piri/pkg/pdp/aggregation/manager/accepter.go index f4a5032d..d01cbf05 100644 --- a/piri/pkg/pdp/aggregation/manager/accepter.go +++ b/piri/pkg/pdp/aggregation/manager/accepter.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/fil-forge/libforge/commands/pdp" + "github.com/fil-forge/forge/protocol/commands/pdp" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/fil-forge/ucantone/ucan/receipt" diff --git a/piri/pkg/pdp/httpapi/client/client.go b/piri/pkg/pdp/httpapi/client/client.go index 5c16a240..aaa39bd2 100644 --- a/piri/pkg/pdp/httpapi/client/client.go +++ b/piri/pkg/pdp/httpapi/client/client.go @@ -14,7 +14,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/multikey" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" diff --git a/piri/pkg/pdp/piece/resolver.go b/piri/pkg/pdp/piece/resolver.go index aa3ea332..f0a8c9fd 100644 --- a/piri/pkg/pdp/piece/resolver.go +++ b/piri/pkg/pdp/piece/resolver.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" commcid "github.com/filecoin-project/go-fil-commcid" lru "github.com/hashicorp/golang-lru/v2" "github.com/ipfs/go-cid" diff --git a/piri/pkg/pdp/piece/resolver_test.go b/piri/pkg/pdp/piece/resolver_test.go index 8f68bea7..ce4c6d10 100644 --- a/piri/pkg/pdp/piece/resolver_test.go +++ b/piri/pkg/pdp/piece/resolver_test.go @@ -11,9 +11,9 @@ import ( "github.com/multiformats/go-multihash" "github.com/stretchr/testify/require" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" ) // NOTE: the StoreResolver's database path (commP<->blob mapping over diff --git a/piri/pkg/pdp/piecesize/piecesize.go b/piri/pkg/pdp/piecesize/piecesize.go index bb719185..56088d76 100644 --- a/piri/pkg/pdp/piecesize/piecesize.go +++ b/piri/pkg/pdp/piecesize/piecesize.go @@ -46,7 +46,7 @@ import ( "github.com/filecoin-project/curio/lib/proof" "github.com/filecoin-project/go-state-types/abi" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" ) const ( @@ -57,7 +57,7 @@ const ( // MinPaddedSize is the smallest configurable maximum, equal to // DefaultMaxPaddedSize. The limit is raise-only: network clients shard - // uploads against a network-wide constant (libforge blob.MaxBlobSize), so + // uploads against a network-wide constant (protocol blob.MaxBlobSize), so // a node configured below it would reject blobs every other node accepts. // An operator may accept more than the network default, never less. MinPaddedSize uint64 = 1 << 28 diff --git a/piri/pkg/pdp/pipeline/task_aggregate.go b/piri/pkg/pdp/pipeline/task_aggregate.go index b1adfd41..92858dfa 100644 --- a/piri/pkg/pdp/pipeline/task_aggregate.go +++ b/piri/pkg/pdp/pipeline/task_aggregate.go @@ -12,7 +12,7 @@ import ( "github.com/ipfs/go-cid" "github.com/multiformats/go-multihash" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" "github.com/fil-forge/forge/piri/pkg/pdp/aggregation/aggregator" aggtypes "github.com/fil-forge/forge/piri/pkg/pdp/aggregation/types" diff --git a/piri/pkg/pdp/service/piece_commp_test.go b/piri/pkg/pdp/service/piece_commp_test.go index d4f8f083..2532ddd1 100644 --- a/piri/pkg/pdp/service/piece_commp_test.go +++ b/piri/pkg/pdp/service/piece_commp_test.go @@ -5,7 +5,7 @@ import ( "io" "testing" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" commcid "github.com/filecoin-project/go-fil-commcid" commp "github.com/filecoin-project/go-fil-commp-hashhash" "github.com/ipfs/go-cid" diff --git a/piri/pkg/pdp/service/piece_remove_test.go b/piri/pkg/pdp/service/piece_remove_test.go index aed43d5e..8a73fa9f 100644 --- a/piri/pkg/pdp/service/piece_remove_test.go +++ b/piri/pkg/pdp/service/piece_remove_test.go @@ -14,8 +14,8 @@ import ( "github.com/multiformats/go-multihash" "github.com/stretchr/testify/require" - blobcmd "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" + blobcmd "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/promise" piritestutil "github.com/fil-forge/forge/piri/pkg/internal/testutil" diff --git a/piri/pkg/pdp/service/piece_upload.go b/piri/pkg/pdp/service/piece_upload.go index fd0eabc4..f05a1cfd 100644 --- a/piri/pkg/pdp/service/piece_upload.go +++ b/piri/pkg/pdp/service/piece_upload.go @@ -13,7 +13,7 @@ import ( "github.com/filecoin-project/curio/harmony/harmonydb" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" "github.com/fil-forge/forge/piri/lib/verifyread" "github.com/fil-forge/forge/piri/pkg/pdp/types" "github.com/fil-forge/forge/piri/pkg/presets" diff --git a/piri/pkg/pdp/service/roots_add.go b/piri/pkg/pdp/service/roots_add.go index 1d01265e..5ea17dd2 100644 --- a/piri/pkg/pdp/service/roots_add.go +++ b/piri/pkg/pdp/service/roots_add.go @@ -11,7 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/fil-forge/filecoin-services/go/eip712" - libforgesign "github.com/fil-forge/libforge/commands/pdp/sign" + libforgesign "github.com/fil-forge/forge/protocol/commands/pdp/sign" "github.com/filecoin-project/curio/harmony/harmonydb" "github.com/filecoin-project/go-commp-utils/nonffi" commcid "github.com/filecoin-project/go-fil-commcid" @@ -528,7 +528,7 @@ func (p *PDPService) AddRoots(ctx context.Context, id uint64, request []types.Ro } // TODO(forrest)[ucan1] #9: rebuild proof bundling on top of ucantone receipts + - // libforge pdp/sign.PieceProofs. The previous go-ucanto path collected + // protocol pdp/sign.PieceProofs. The previous go-ucanto path collected // blob/accept and pdp/accept invocations + receipts into per-piece agent // messages; that requires the receipt/acceptance stores to be migrated // first. Until then we send empty piece proofs and a nil proof diff --git a/piri/pkg/pdp/service/service.go b/piri/pkg/pdp/service/service.go index c74ad5e2..0d9c594e 100644 --- a/piri/pkg/pdp/service/service.go +++ b/piri/pkg/pdp/service/service.go @@ -7,7 +7,7 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/fil-forge/filecoin-services/go/eip712" - signer "github.com/fil-forge/piri-signing-service/pkg/types" + signer "github.com/fil-forge/forge/internal/pdpsigner/types" "github.com/fil-forge/ucantone/ucan" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/lotus/api" diff --git a/piri/pkg/service/egresstracker/service.go b/piri/pkg/service/egresstracker/service.go index 8fbe947d..130b0c8c 100644 --- a/piri/pkg/service/egresstracker/service.go +++ b/piri/pkg/service/egresstracker/service.go @@ -15,8 +15,8 @@ import ( "github.com/fil-forge/go-ucanto/client" "github.com/fil-forge/go-ucanto/core/dag/blockstore" "github.com/fil-forge/go-ucanto/core/result" - "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/commands/space/egress" + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/forge/protocol/commands/space/egress" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey" "github.com/fil-forge/ucantone/ucan" diff --git a/piri/pkg/service/egresstracker/service_test.go b/piri/pkg/service/egresstracker/service_test.go index 5cee95e2..f1d6e36e 100644 --- a/piri/pkg/service/egresstracker/service_test.go +++ b/piri/pkg/service/egresstracker/service_test.go @@ -172,7 +172,7 @@ func createTestReceipt(t *testing.T, client ucan.Signer, node ucan.Signer) recei space.String(), content.RetrieveCaveats{ Blob: content.BlobDigest{ - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), }, Range: content.Range{ Start: 1024, diff --git a/piri/pkg/service/proofs/caching.go b/piri/pkg/service/proofs/caching.go index b74ea535..79b19fb1 100644 --- a/piri/pkg/service/proofs/caching.go +++ b/piri/pkg/service/proofs/caching.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "github.com/fil-forge/libforge/commands/access" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/execution" diff --git a/piri/pkg/service/proofs/caching_test.go b/piri/pkg/service/proofs/caching_test.go index 3fdb0cbb..68cac649 100644 --- a/piri/pkg/service/proofs/caching_test.go +++ b/piri/pkg/service/proofs/caching_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/fil-forge/libforge/commands/access" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/server" diff --git a/piri/pkg/service/publisher/fx.go b/piri/pkg/service/publisher/fx.go index b490c2bf..fbbf60ca 100644 --- a/piri/pkg/service/publisher/fx.go +++ b/piri/pkg/service/publisher/fx.go @@ -3,8 +3,8 @@ package publisher import ( "fmt" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/go-ipni-tools/pkg/store" - "github.com/fil-forge/libforge/identity" "go.uber.org/fx" "github.com/fil-forge/forge/piri/pkg/config/app" diff --git a/piri/pkg/service/publisher/publisher.go b/piri/pkg/service/publisher/publisher.go index 117d510b..3adc73d7 100644 --- a/piri/pkg/service/publisher/publisher.go +++ b/piri/pkg/service/publisher/publisher.go @@ -22,12 +22,12 @@ import ( "github.com/multiformats/go-multiaddr" "github.com/multiformats/go-multihash" - "github.com/fil-forge/go-ipni-tools/pkg/advertisement" + "github.com/fil-forge/forge/internal/ipni/advertisement" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/claim" "github.com/fil-forge/go-ipni-tools/pkg/metadata" ipnipub "github.com/fil-forge/go-ipni-tools/pkg/publisher" "github.com/fil-forge/go-ipni-tools/pkg/store" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/claim" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/fil-forge/forge/piri/lib" diff --git a/piri/pkg/service/publisher/publisher_test.go b/piri/pkg/service/publisher/publisher_test.go index 4f051194..67cd639e 100644 --- a/piri/pkg/service/publisher/publisher_test.go +++ b/piri/pkg/service/publisher/publisher_test.go @@ -6,18 +6,18 @@ import ( "net/url" "testing" - "github.com/fil-forge/go-ipni-tools/pkg/advertisement" + "github.com/fil-forge/forge/internal/ipni/advertisement" + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/claim" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/go-ipni-tools/pkg/metadata" "github.com/fil-forge/go-ipni-tools/pkg/store" - "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/claim" - "github.com/fil-forge/libforge/digestutil" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/delegation" "github.com/fil-forge/ucantone/ucan/invocation" @@ -45,7 +45,7 @@ func TestPublisherService(t *testing.T) { require.NoError(t, err) space := testutil.RandomDID(t) - shard := testutil.RandomMultihash(t) + shard := testutil.RandomDigest(t) location := testutil.Must(url.Parse(fmt.Sprintf("http://localhost:3000/blob/%s", digestutil.Format(shard))))(t) claimInv := mintLocationClaim(t, space, shard, *location) @@ -87,7 +87,7 @@ func TestPublisherService(t *testing.T) { require.NoError(t, err) space := testutil.RandomDID(t) - shard := testutil.RandomMultihash(t) + shard := testutil.RandomDigest(t) location := testutil.Must(url.Parse(fmt.Sprintf("http://localhost:3000/blob/%s", digestutil.Format(shard))))(t) claimInv := mintLocationClaim(t, space, shard, *location) @@ -146,7 +146,7 @@ func TestPublisherService(t *testing.T) { require.NoError(t, err) space := testutil.RandomDID(t) - shard := testutil.RandomMultihash(t) + shard := testutil.RandomDigest(t) location := testutil.Must(url.Parse(fmt.Sprintf("http://localhost:3000/blob/%s", digestutil.Format(shard))))(t) claimInv := mintLocationClaim(t, space, shard, *location) diff --git a/piri/pkg/service/signer/proofservicesigner.go b/piri/pkg/service/signer/proofservicesigner.go index 6e4dc225..200127bb 100644 --- a/piri/pkg/service/signer/proofservicesigner.go +++ b/piri/pkg/service/signer/proofservicesigner.go @@ -7,9 +7,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/fil-forge/filecoin-services/go/eip712" - "github.com/fil-forge/libforge/commands/pdp/sign" - signerclient "github.com/fil-forge/piri-signing-service/pkg/client" - signertypes "github.com/fil-forge/piri-signing-service/pkg/types" + signerclient "github.com/fil-forge/forge/internal/pdpsigner/client" + signertypes "github.com/fil-forge/forge/internal/pdpsigner/types" + "github.com/fil-forge/forge/protocol/commands/pdp/sign" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" diff --git a/piri/pkg/service/signer/proofservicesigner_test.go b/piri/pkg/service/signer/proofservicesigner_test.go index 885b06b5..04e19a62 100644 --- a/piri/pkg/service/signer/proofservicesigner_test.go +++ b/piri/pkg/service/signer/proofservicesigner_test.go @@ -10,9 +10,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/fil-forge/filecoin-services/go/eip712" - "github.com/fil-forge/libforge/commands/access" - libforgesign "github.com/fil-forge/libforge/commands/pdp/sign" - signerclient "github.com/fil-forge/piri-signing-service/pkg/client" + signerclient "github.com/fil-forge/forge/internal/pdpsigner/client" + "github.com/fil-forge/forge/protocol/commands/access" + libforgesign "github.com/fil-forge/forge/protocol/commands/pdp/sign" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/server" diff --git a/piri/pkg/store/acceptancestore/store.go b/piri/pkg/store/acceptancestore/store.go index 736c0f13..5eafd770 100644 --- a/piri/pkg/store/acceptancestore/store.go +++ b/piri/pkg/store/acceptancestore/store.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/fil-forge/libforge/digestutil" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/ucantone/did" "github.com/ipfs/go-datastore" "github.com/multiformats/go-multihash" diff --git a/piri/pkg/store/acceptancestore/store_test.go b/piri/pkg/store/acceptancestore/store_test.go index 1871fe1f..3c31837f 100644 --- a/piri/pkg/store/acceptancestore/store_test.go +++ b/piri/pkg/store/acceptancestore/store_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/promise" "github.com/ipfs/go-datastore" "github.com/stretchr/testify/require" @@ -22,7 +22,7 @@ func TestDsAcceptanceStore(t *testing.T) { acc := acceptance.Acceptance{ Space: testutil.RandomDID(t), Blob: acceptance.Blob{ - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), Size: uint64(1 + rand.IntN(1000)), }, ExecutedAt: uint64(time.Now().Unix()), @@ -50,7 +50,7 @@ func TestDsAcceptanceStore(t *testing.T) { t.Run("multiple", func(t *testing.T) { s := acceptancestore.NewDatastoreStore(datastore.NewMapDatastore()) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) acc0 := acceptance.Acceptance{ Space: testutil.RandomDID(t), @@ -101,7 +101,7 @@ func TestDsAcceptanceStore(t *testing.T) { t.Run("not found", func(t *testing.T) { s := acceptancestore.NewDatastoreStore(datastore.NewMapDatastore()) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) _, err := s.Get(t.Context(), digest, space) diff --git a/piri/pkg/store/allocationstore/allocation/allocation.go b/piri/pkg/store/allocationstore/allocation/allocation.go index dd7a5ca7..3e3b03e5 100644 --- a/piri/pkg/store/allocationstore/allocation/allocation.go +++ b/piri/pkg/store/allocationstore/allocation/allocation.go @@ -5,7 +5,7 @@ import ( // for go:embed _ "embed" - "github.com/fil-forge/libforge/commands/blob" + "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" "github.com/ipfs/go-cid" diff --git a/piri/pkg/store/allocationstore/store.go b/piri/pkg/store/allocationstore/store.go index 1d7284c5..7758784a 100644 --- a/piri/pkg/store/allocationstore/store.go +++ b/piri/pkg/store/allocationstore/store.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/fil-forge/libforge/digestutil" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" "github.com/ipfs/go-datastore" diff --git a/piri/pkg/store/allocationstore/store_test.go b/piri/pkg/store/allocationstore/store_test.go index 64b4db8c..937d6276 100644 --- a/piri/pkg/store/allocationstore/store_test.go +++ b/piri/pkg/store/allocationstore/store_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/ipfs/go-datastore" "github.com/stretchr/testify/require" @@ -22,7 +22,7 @@ func TestDatastoreAllocationStore(t *testing.T) { alloc := allocation.Allocation{ Space: testutil.RandomDID(t), Blob: blob.Blob{ - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), Size: uint64(1 + rand.IntN(1000)), }, Expires: ucan.UnixTimestamp(time.Now().Unix()), @@ -43,7 +43,7 @@ func TestDatastoreAllocationStore(t *testing.T) { alloc := allocation.Allocation{ Space: testutil.RandomDID(t), Blob: blob.Blob{ - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), Size: uint64(1 + rand.IntN(1000)), }, Expires: ucan.UnixTimestamp(time.Now().Unix()), @@ -64,7 +64,7 @@ func TestDatastoreAllocationStore(t *testing.T) { alloc := allocation.Allocation{ Space: testutil.RandomDID(t), Blob: blob.Blob{ - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), Size: uint64(1 + rand.IntN(1000)), }, Expires: ucan.UnixTimestamp(time.Now().Unix()), @@ -87,7 +87,7 @@ func TestDatastoreAllocationStore(t *testing.T) { s := NewDatastoreStore(datastore.NewMapDatastore()) blb := blob.Blob{ - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), Size: uint64(1 + rand.IntN(1000)), } @@ -133,7 +133,7 @@ func TestDatastoreAllocationStore(t *testing.T) { t.Run("not found", func(t *testing.T) { s := NewDatastoreStore(datastore.NewMapDatastore()) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) _, err := s.Get(t.Context(), digest, space) @@ -147,7 +147,7 @@ func TestDatastoreAllocationStore(t *testing.T) { s := NewDatastoreStore(datastore.NewMapDatastore()) blb := blob.Blob{ - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), Size: uint64(1 + rand.IntN(1000)), } @@ -184,7 +184,7 @@ func TestDatastoreAllocationStore(t *testing.T) { s := NewDatastoreStore(datastore.NewMapDatastore()) blb := blob.Blob{ - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), Size: uint64(1 + rand.IntN(1000)), } @@ -207,7 +207,7 @@ func TestDatastoreAllocationStore(t *testing.T) { t.Run("get any non-expired not found", func(t *testing.T) { s := NewDatastoreStore(datastore.NewMapDatastore()) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) now := ucan.Now() _, err := s.GetAnyNonExpired(t.Context(), digest, now) diff --git a/piri/pkg/store/blobstore/key_encoder.go b/piri/pkg/store/blobstore/key_encoder.go index fdaf9d99..a1f8b51f 100644 --- a/piri/pkg/store/blobstore/key_encoder.go +++ b/piri/pkg/store/blobstore/key_encoder.go @@ -3,8 +3,8 @@ package blobstore import ( "path/filepath" - "github.com/fil-forge/libforge/digestutil" "github.com/fil-forge/forge/piri/pkg/store/objectstore/flatfs" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/multiformats/go-multibase" "github.com/multiformats/go-multihash" ) diff --git a/piri/pkg/store/blobstore/store_test.go b/piri/pkg/store/blobstore/store_test.go index ba7d42c1..a2ede749 100644 --- a/piri/pkg/store/blobstore/store_test.go +++ b/piri/pkg/store/blobstore/store_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/google/uuid" "github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore/sync" diff --git a/piri/pkg/store/consolidationstore/consolidation/consolidation_test.go b/piri/pkg/store/consolidationstore/consolidation/consolidation_test.go index 3a8c0d5c..9976c94f 100644 --- a/piri/pkg/store/consolidationstore/consolidation/consolidation_test.go +++ b/piri/pkg/store/consolidationstore/consolidation/consolidation_test.go @@ -3,8 +3,8 @@ package consolidation_test import ( "testing" - "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/commands/space/egress" + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/forge/protocol/commands/space/egress" "github.com/fil-forge/ucantone/testutil" "github.com/stretchr/testify/require" diff --git a/piri/pkg/store/consolidationstore/store_test.go b/piri/pkg/store/consolidationstore/store_test.go index db52fbfd..de9ef1c5 100644 --- a/piri/pkg/store/consolidationstore/store_test.go +++ b/piri/pkg/store/consolidationstore/store_test.go @@ -3,8 +3,8 @@ package consolidationstore import ( "testing" - "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/commands/space/egress" + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/forge/protocol/commands/space/egress" "github.com/fil-forge/ucantone/testutil" "github.com/ipfs/go-cid" "github.com/ipfs/go-datastore" diff --git a/piri/pkg/store/invocationstore/store_test.go b/piri/pkg/store/invocationstore/store_test.go index ad937bbf..5607dcab 100644 --- a/piri/pkg/store/invocationstore/store_test.go +++ b/piri/pkg/store/invocationstore/store_test.go @@ -3,8 +3,8 @@ package invocationstore import ( "testing" - "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/command" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/ipfs/go-datastore" diff --git a/piri/pkg/store/local/retrievaljournal/fsjournal_test.go b/piri/pkg/store/local/retrievaljournal/fsjournal_test.go index b326fc43..9bbfef9b 100644 --- a/piri/pkg/store/local/retrievaljournal/fsjournal_test.go +++ b/piri/pkg/store/local/retrievaljournal/fsjournal_test.go @@ -11,8 +11,8 @@ import ( "sync/atomic" "testing" - "github.com/fil-forge/libforge/commands/content" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands/content" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/receipt" "github.com/ipfs/go-cid" @@ -29,7 +29,7 @@ func createTestReceipt(t *testing.T) ucan.Receipt { node.DID(), &content.RetrieveArguments{ Blob: content.Blob{ - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), }, Range: content.Range{ Start: 1024, diff --git a/piri/pkg/store/local/retrievaljournal/periodic_rotator_test.go b/piri/pkg/store/local/retrievaljournal/periodic_rotator_test.go index 4bc38eaa..595a82a2 100644 --- a/piri/pkg/store/local/retrievaljournal/periodic_rotator_test.go +++ b/piri/pkg/store/local/retrievaljournal/periodic_rotator_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" diff --git a/piri/pkg/ucanhandlers/access/grant.go b/piri/pkg/ucanhandlers/access/grant.go index ec300476..6528f604 100644 --- a/piri/pkg/ucanhandlers/access/grant.go +++ b/piri/pkg/ucanhandlers/access/grant.go @@ -15,11 +15,11 @@ import ( "go.opentelemetry.io/otel/codes" fxlib "go.uber.org/fx" - "github.com/fil-forge/libforge/commands/access" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/commands/blob/replica" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/access" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/commands/blob/replica" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/ucan" diff --git a/piri/pkg/ucanhandlers/blob/accept.go b/piri/pkg/ucanhandlers/blob/accept.go index 007736cb..f43923be 100644 --- a/piri/pkg/ucanhandlers/blob/accept.go +++ b/piri/pkg/ucanhandlers/blob/accept.go @@ -6,8 +6,8 @@ import ( "net/url" "time" - "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" "github.com/fil-forge/ucantone/ucan/container" @@ -17,10 +17,10 @@ import ( "go.opentelemetry.io/otel/codes" "go.uber.org/fx" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/commands/pdp" - "github.com/fil-forge/libforge/digestutil" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/commands/pdp" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/invocation" diff --git a/piri/pkg/ucanhandlers/blob/accept_test.go b/piri/pkg/ucanhandlers/blob/accept_test.go index 2b6abaf6..529488dd 100644 --- a/piri/pkg/ucanhandlers/blob/accept_test.go +++ b/piri/pkg/ucanhandlers/blob/accept_test.go @@ -5,10 +5,10 @@ import ( "errors" "testing" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/identity" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" @@ -52,7 +52,7 @@ func TestAccept_PieceMissing(t *testing.T) { _, err := Accept(t.Context(), deps, &AcceptRequest{ Space: testutil.RandomDID(t), - Blob: blob.Blob{Digest: testutil.RandomMultihash(t), Size: 4}, + Blob: blob.Blob{Digest: testutil.RandomDigest(t), Size: 4}, Cause: testutil.RandomCID(t), }) require.Error(t, err) @@ -62,7 +62,7 @@ func TestAccept_PieceMissing(t *testing.T) { func TestAccept_PieceHeld(t *testing.T) { deps, pieces, accepts, claimStore, pub := newAcceptDeps(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) cause := testutil.RandomCID(t) pieces.Put(digest, []byte("data")) @@ -123,7 +123,7 @@ func TestAccept_AcceptanceDurableBeforeEnqueue(t *testing.T) { probe := &orderProbeCommp{accepts: accepts} deps.Commp = probe - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) pieces.Put(digest, []byte("data")) _, err := Accept(t.Context(), deps, &AcceptRequest{ @@ -142,7 +142,7 @@ func TestAccept_EnqueueFailureCompensatesAcceptance(t *testing.T) { probe := &orderProbeCommp{accepts: accepts, fail: true} deps.Commp = probe - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) pieces.Put(digest, []byte("data")) _, err := Accept(t.Context(), deps, &AcceptRequest{ diff --git a/piri/pkg/ucanhandlers/blob/allocate.go b/piri/pkg/ucanhandlers/blob/allocate.go index e70d2bd2..e2213666 100644 --- a/piri/pkg/ucanhandlers/blob/allocate.go +++ b/piri/pkg/ucanhandlers/blob/allocate.go @@ -5,7 +5,7 @@ import ( "fmt" "net/url" - "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/forge/protocol/commands" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" "github.com/google/uuid" @@ -16,8 +16,8 @@ import ( "go.opentelemetry.io/otel/codes" "go.uber.org/fx" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/digestutil" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/ucan" diff --git a/piri/pkg/ucanhandlers/blob/allocate_test.go b/piri/pkg/ucanhandlers/blob/allocate_test.go index bcecdf61..bccf77b3 100644 --- a/piri/pkg/ucanhandlers/blob/allocate_test.go +++ b/piri/pkg/ucanhandlers/blob/allocate_test.go @@ -3,9 +3,9 @@ package blob import ( "testing" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands/blob" ucanerrors "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/testutil" "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" "github.com/multiformats/go-multihash" @@ -33,7 +33,7 @@ func newAllocateDeps(t *testing.T) (AllocateDeps, *pdpfake.Pieces, *allocationst func TestAllocate_NoExistingAllocationOrBlob(t *testing.T) { deps, _, allocs := newAllocateDeps(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) cause := testutil.RandomCID(t) @@ -55,7 +55,7 @@ func TestAllocate_NoExistingAllocationOrBlob(t *testing.T) { func TestAllocate_ExistingAllocationButBlobNotReceived(t *testing.T) { deps, _, allocs := newAllocateDeps(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) require.NoError(t, allocs.Put(t.Context(), allocation.Allocation{ @@ -76,7 +76,7 @@ func TestAllocate_ExistingAllocationButBlobNotReceived(t *testing.T) { func TestAllocate_BlobAlreadyReceivedInSameSpace(t *testing.T) { deps, pieces, allocs := newAllocateDeps(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) require.NoError(t, allocs.Put(t.Context(), allocation.Allocation{ @@ -101,7 +101,7 @@ func TestAllocate_BlobInOtherSpace(t *testing.T) { // in *this* space yet. We need a new allocation (size = blob size) but no // upload URL. deps, pieces, allocs := newAllocateDeps(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) otherSpace := testutil.RandomDID(t) thisSpace := testutil.RandomDID(t) @@ -131,7 +131,7 @@ func TestAllocate_BlobPendingRemovalNoAllocations(t *testing.T) { // wrapping an empty URL sends the client a PUT to "". The allocation // row written here is a claim, so the sweep cancels the queued removal. deps, pieces, allocs := newAllocateDeps(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) pieces.Put(digest, []byte("data")) // bytes present, zero allocations @@ -173,7 +173,7 @@ func TestAllocate_SizeLimitBoundary(t *testing.T) { t.Run("at the limit", func(t *testing.T) { deps, _, allocs := newAllocateDeps(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) resp, err := Allocate(t.Context(), deps, &AllocateRequest{ @@ -193,7 +193,7 @@ func TestAllocate_SizeLimitBoundary(t *testing.T) { // failed deep inside AllocatePiece, surfacing as a transport error // rather than a receipt the caller could interpret. deps, pieces, allocs := newAllocateDeps(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) _, err := Allocate(t.Context(), deps, &AllocateRequest{ @@ -232,7 +232,7 @@ func TestAllocate_UsesConfiguredPolicy(t *testing.T) { alloc := func(size uint64) error { _, err := Allocate(t.Context(), deps, &AllocateRequest{ Space: testutil.RandomDID(t), - Blob: blob.Blob{Digest: testutil.RandomMultihash(t), Size: size}, + Blob: blob.Blob{Digest: testutil.RandomDigest(t), Size: size}, Cause: testutil.RandomCID(t), }) return err diff --git a/piri/pkg/ucanhandlers/blob/reject.go b/piri/pkg/ucanhandlers/blob/reject.go index 68204d7e..8098100a 100644 --- a/piri/pkg/ucanhandlers/blob/reject.go +++ b/piri/pkg/ucanhandlers/blob/reject.go @@ -9,9 +9,9 @@ import ( "go.opentelemetry.io/otel/codes" "go.uber.org/fx" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/digestutil" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/errors" diff --git a/piri/pkg/ucanhandlers/blob/reject_test.go b/piri/pkg/ucanhandlers/blob/reject_test.go index e7b2ed99..76be7a7a 100644 --- a/piri/pkg/ucanhandlers/blob/reject_test.go +++ b/piri/pkg/ucanhandlers/blob/reject_test.go @@ -3,9 +3,9 @@ package blob import ( "testing" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands/blob" ucanerrors "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/promise" "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" @@ -45,7 +45,7 @@ func newRejectWorld(t *testing.T) *rejectWorld { func TestReject_ParkedBlobDeletesBytes(t *testing.T) { w := newRejectWorld(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) require.NoError(t, w.allocs.Put(t.Context(), allocation.Allocation{ @@ -69,13 +69,13 @@ func TestReject_UnknownBlobIsSuccess(t *testing.T) { w := newRejectWorld(t) require.NoError(t, Reject(t.Context(), w.deps, &RejectRequest{ Space: testutil.RandomDID(t), - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), })) } func TestReject_AcceptedBlobRefused(t *testing.T) { w := newRejectWorld(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) require.NoError(t, w.allocs.Put(t.Context(), allocation.Allocation{ @@ -108,7 +108,7 @@ func TestReject_AcceptedBlobRefused(t *testing.T) { // strand this space's parked allocation (RFC: multi-tenant liveness). func TestReject_OtherSpaceAcceptanceDoesNotBlock(t *testing.T) { w := newRejectWorld(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) rejecting := testutil.RandomDID(t) accepted := testutil.RandomDID(t) @@ -138,7 +138,7 @@ func TestReject_OtherSpaceAcceptanceDoesNotBlock(t *testing.T) { func TestReject_OtherSpaceAllocationRetainsBytes(t *testing.T) { w := newRejectWorld(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) abandoning := testutil.RandomDID(t) uploading := testutil.RandomDID(t) diff --git a/piri/pkg/ucanhandlers/blob/release.go b/piri/pkg/ucanhandlers/blob/release.go index 4b20f5e4..f7fe00aa 100644 --- a/piri/pkg/ucanhandlers/blob/release.go +++ b/piri/pkg/ucanhandlers/blob/release.go @@ -10,9 +10,9 @@ import ( "go.opentelemetry.io/otel/codes" "go.uber.org/fx" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/digestutil" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/errors" diff --git a/piri/pkg/ucanhandlers/blob/release_test.go b/piri/pkg/ucanhandlers/blob/release_test.go index 3223c052..45dfb98f 100644 --- a/piri/pkg/ucanhandlers/blob/release_test.go +++ b/piri/pkg/ucanhandlers/blob/release_test.go @@ -3,9 +3,9 @@ package blob import ( "testing" - "github.com/fil-forge/libforge/commands" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/command" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/fil-forge/ucantone/ucan/promise" @@ -56,7 +56,7 @@ func TestRelease_UnknownBlobIsSuccess(t *testing.T) { err := Release(t.Context(), w.deps, &ReleaseRequest{ Space: testutil.RandomDID(t), - Digest: testutil.RandomMultihash(t), + Digest: testutil.RandomDigest(t), }) require.NoError(t, err, "removing a blob that was never stored is idempotent success") // Nothing claimed the digest, so removal of the (nonexistent) bytes is requested. @@ -65,7 +65,7 @@ func TestRelease_UnknownBlobIsSuccess(t *testing.T) { func TestRelease_ReleasesClaimAndBytes(t *testing.T) { w := newRemoveWorld(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) claim, err := invocation.Invoke( @@ -107,7 +107,7 @@ func TestRelease_ReleasesClaimAndBytes(t *testing.T) { func TestRelease_OtherSpaceClaimRetainsBytes(t *testing.T) { w := newRemoveWorld(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) removingSpace := testutil.RandomDID(t) otherSpace := testutil.RandomDID(t) @@ -144,7 +144,7 @@ func TestRelease_LiveAllocationRetainsBytes(t *testing.T) { // An allocation from another space (upload possibly in flight) blocks // physical deletion even when no acceptance exists. w := newRemoveWorld(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) removingSpace := testutil.RandomDID(t) uploadingSpace := testutil.RandomDID(t) diff --git a/piri/pkg/ucanhandlers/blob/replica/allocate.go b/piri/pkg/ucanhandlers/blob/replica/allocate.go index 4190da79..991de92e 100644 --- a/piri/pkg/ucanhandlers/blob/replica/allocate.go +++ b/piri/pkg/ucanhandlers/blob/replica/allocate.go @@ -9,9 +9,9 @@ import ( "net/url" "time" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/commands/blob/replica" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/commands/blob/replica" "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/multikey" diff --git a/piri/pkg/ucanhandlers/blob/replica/transfer.go b/piri/pkg/ucanhandlers/blob/replica/transfer.go index f8285fb4..564c6074 100644 --- a/piri/pkg/ucanhandlers/blob/replica/transfer.go +++ b/piri/pkg/ucanhandlers/blob/replica/transfer.go @@ -29,11 +29,11 @@ import ( "go.opentelemetry.io/otel/attribute" fxlib "go.uber.org/fx" - "github.com/fil-forge/libforge/commands/access" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/commands/blob/replica" - ucancap "github.com/fil-forge/libforge/commands/ucan" + "github.com/fil-forge/forge/protocol/commands/access" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/commands/blob/replica" + ucancap "github.com/fil-forge/forge/protocol/commands/ucan" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey" diff --git a/piri/pkg/ucanhandlers/blob/retrieve.go b/piri/pkg/ucanhandlers/blob/retrieve.go index 999cdd45..8258e189 100644 --- a/piri/pkg/ucanhandlers/blob/retrieve.go +++ b/piri/pkg/ucanhandlers/blob/retrieve.go @@ -8,8 +8,8 @@ import ( "net/http" "strings" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/ucan/retrieval" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/retrieval" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/server" diff --git a/piri/pkg/ucanhandlers/blob/retrieve_test.go b/piri/pkg/ucanhandlers/blob/retrieve_test.go index cc0bbfe6..6b6fe1ad 100644 --- a/piri/pkg/ucanhandlers/blob/retrieve_test.go +++ b/piri/pkg/ucanhandlers/blob/retrieve_test.go @@ -7,7 +7,7 @@ import ( "strconv" "testing" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/ucantone/testutil" "github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore/sync" "github.com/multiformats/go-multihash" @@ -43,7 +43,7 @@ func newPieceReader(t *testing.T, data ...[]byte) (types.PieceReaderAPI, []multi func TestRetrieve_NotFound(t *testing.T) { reader, _ := newPieceReader(t) - container, err := Retrieve(t.Context(), reader, testutil.RandomMultihash(t), nil) + container, err := Retrieve(t.Context(), reader, testutil.RandomDigest(t), nil) require.Error(t, err, "missing blob should surface as a named UCAN error") require.Equal(t, NotFoundErrorName, namedError(t, err)) diff --git a/piri/pkg/ucanhandlers/content/retrieve.go b/piri/pkg/ucanhandlers/content/retrieve.go index 799b221c..88c85453 100644 --- a/piri/pkg/ucanhandlers/content/retrieve.go +++ b/piri/pkg/ucanhandlers/content/retrieve.go @@ -6,7 +6,7 @@ import ( "net/http" "os" - "github.com/fil-forge/libforge/commands/content" + "github.com/fil-forge/forge/protocol/commands/content" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" logging "github.com/ipfs/go-log/v2" diff --git a/piri/pkg/ucanhandlers/handler.go b/piri/pkg/ucanhandlers/handler.go index af477c93..27fbd057 100644 --- a/piri/pkg/ucanhandlers/handler.go +++ b/piri/pkg/ucanhandlers/handler.go @@ -3,8 +3,8 @@ package ucanhandlers import ( "fmt" - "github.com/fil-forge/libforge/identity" - "github.com/fil-forge/libforge/ucan/retrieval" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/retrieval" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/server" "github.com/fil-forge/ucantone/ucan" diff --git a/piri/pkg/ucanhandlers/pdp/info.go b/piri/pkg/ucanhandlers/pdp/info.go index 169d35f8..dfcca5a4 100644 --- a/piri/pkg/ucanhandlers/pdp/info.go +++ b/piri/pkg/ucanhandlers/pdp/info.go @@ -11,12 +11,12 @@ import ( "github.com/multiformats/go-multihash" fxlib "go.uber.org/fx" - "github.com/fil-forge/libforge/commands/pdp" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/pdp" "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/ucan/invocation" - libpiece "github.com/fil-forge/libforge/piece" + libpiece "github.com/fil-forge/forge/internal/piece" pdptypes "github.com/fil-forge/forge/piri/pkg/pdp/types" "github.com/fil-forge/forge/piri/pkg/store/receiptstore" ) diff --git a/piri/pkg/ucanhandlers/ucanfxtest/base/client.go b/piri/pkg/ucanhandlers/ucanfxtest/base/client.go index ceb1b779..e73cadbd 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/base/client.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/base/client.go @@ -3,7 +3,7 @@ package base import ( "testing" - "github.com/fil-forge/libforge/ucan/retrieval" + "github.com/fil-forge/forge/protocol/retrieval" "github.com/fil-forge/ucantone/client" "github.com/stretchr/testify/require" ) @@ -19,7 +19,7 @@ func (s *BaseSuite) RPCClient(t *testing.T) *client.HTTPClient { return c } -// RetrievalClient returns a libforge retrieval client pointed at the +// RetrievalClient returns a protocol retrieval client pointed at the // suite's byte-streaming endpoint (GET /piece/:cid). Use this for // capabilities whose response body is the raw blob bytes: // blob/retrieve, content/retrieve. diff --git a/piri/pkg/ucanhandlers/ucanfxtest/retrieval/blob_test.go b/piri/pkg/ucanhandlers/ucanfxtest/retrieval/blob_test.go index 7b424be9..7f48ebc7 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/retrieval/blob_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/retrieval/blob_test.go @@ -5,10 +5,10 @@ import ( "net/http" "strconv" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" - "github.com/fil-forge/libforge/ucan/retrieval" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/retrieval" "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/ipfs/go-cid" mh "github.com/multiformats/go-multihash" diff --git a/piri/pkg/ucanhandlers/ucanfxtest/retrieval/content_test.go b/piri/pkg/ucanhandlers/ucanfxtest/retrieval/content_test.go index 435c5489..7bbbfd90 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/retrieval/content_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/retrieval/content_test.go @@ -7,12 +7,12 @@ import ( "strconv" "time" - "github.com/fil-forge/libforge/commands/blob" - contentcap "github.com/fil-forge/libforge/commands/content" - "github.com/fil-forge/libforge/testutil" - "github.com/fil-forge/libforge/ucan/retrieval" + "github.com/fil-forge/forge/protocol/commands/blob" + contentcap "github.com/fil-forge/forge/protocol/commands/content" + "github.com/fil-forge/forge/protocol/retrieval" "github.com/fil-forge/ucantone/errors/datamodel" "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/ipfs/go-cid" @@ -87,7 +87,7 @@ func (s *RetrievalSuite) TestContentRetrieve_NotAllocated() { t := s.T() space := testutil.RandomIssuer(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) // intentionally no allocation in s.Allocations and no bytes in s.Pieces inv, err := contentcap.Retrieve.Invoke( diff --git a/piri/pkg/ucanhandlers/ucanfxtest/retrieval/suite_test.go b/piri/pkg/ucanhandlers/ucanfxtest/retrieval/suite_test.go index ce0a938b..68f13ed4 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/retrieval/suite_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/retrieval/suite_test.go @@ -4,11 +4,11 @@ import ( "net/url" "testing" - "github.com/fil-forge/libforge/identity" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/did/key" "github.com/fil-forge/ucantone/did/resolver" + "github.com/fil-forge/ucantone/testutil" "github.com/stretchr/testify/suite" "go.uber.org/fx" diff --git a/piri/pkg/ucanhandlers/ucanfxtest/rpc/access_grant_test.go b/piri/pkg/ucanhandlers/ucanfxtest/rpc/access_grant_test.go index 00fbc130..8084e982 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/rpc/access_grant_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/rpc/access_grant_test.go @@ -5,12 +5,12 @@ package rpc_test // decode the receipt) lives in helpers_test.go. import ( - "github.com/fil-forge/libforge/commands/access" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/commands/blob/replica" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands/access" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/commands/blob/replica" "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/command" "github.com/fil-forge/ucantone/ucan/delegation" @@ -31,7 +31,7 @@ func (s *RPCSuite) TestAccessGrant_UnknownAbility() { s.UploadServiceIdentity, grantee.DID(), &replica.AllocateArguments{ - Blob: blob.Blob{Digest: testutil.RandomMultihash(t), Size: 1234}, + Blob: blob.Blob{Digest: testutil.RandomDigest(t), Size: 1234}, Site: testutil.RandomCID(t), Cause: testutil.RandomCID(t), }, @@ -55,7 +55,7 @@ func (s *RPCSuite) TestAccessGrant_UnknownCauseEnvelope() { s.UploadServiceIdentity, grantee.DID(), &replica.AllocateArguments{ - Blob: blob.Blob{Digest: testutil.RandomMultihash(t), Size: 1234}, + Blob: blob.Blob{Digest: testutil.RandomDigest(t), Size: 1234}, Site: testutil.RandomCID(t), Cause: testutil.RandomCID(t), }, @@ -92,7 +92,7 @@ func (s *RPCSuite) TestAccessGrant_UnknownCauseCommand() { s.UploadServiceIdentity, grantee.DID(), &assert.EqualsArguments{ - Content: testutil.RandomMultihash(t), + Content: testutil.RandomDigest(t), Equals: testutil.RandomCID(t), }, invocation.WithAudience(grantee.DID()), @@ -112,7 +112,7 @@ func (s *RPCSuite) TestAccessGrant_CauseAudienceMismatch() { s.UploadServiceIdentity, grantee.DID(), &replica.AllocateArguments{ - Blob: blob.Blob{Digest: testutil.RandomMultihash(t), Size: 1234}, + Blob: blob.Blob{Digest: testutil.RandomDigest(t), Size: 1234}, Site: testutil.RandomCID(t), Cause: testutil.RandomCID(t), }, @@ -133,7 +133,7 @@ func (s *RPCSuite) TestAccessGrant_CauseIssuerMismatch() { grantee, grantee.DID(), &replica.AllocateArguments{ - Blob: blob.Blob{Digest: testutil.RandomMultihash(t), Size: 1234}, + Blob: blob.Blob{Digest: testutil.RandomDigest(t), Size: 1234}, Site: testutil.RandomCID(t), Cause: testutil.RandomCID(t), }, @@ -156,7 +156,7 @@ func (s *RPCSuite) TestAccessGrant_UnauthorizedCause() { s.UploadServiceIdentity, grantee.DID(), &replica.AllocateArguments{ - Blob: blob.Blob{Digest: testutil.RandomMultihash(t), Size: 1234}, + Blob: blob.Blob{Digest: testutil.RandomDigest(t), Size: 1234}, Site: testutil.RandomCID(t), Cause: testutil.RandomCID(t), }, @@ -187,7 +187,7 @@ func (s *RPCSuite) TestAccessGrant_BlobRetrieve_WithReplicaAllocateCause() { s.UploadServiceIdentity, grantee.DID(), &replica.AllocateArguments{ - Blob: blob.Blob{Digest: testutil.RandomMultihash(t), Size: 1234}, + Blob: blob.Blob{Digest: testutil.RandomDigest(t), Size: 1234}, Site: testutil.RandomCID(t), Cause: testutil.RandomCID(t), }, diff --git a/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_accept_test.go b/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_accept_test.go index bf5c1854..36d3334b 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_accept_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_accept_test.go @@ -4,10 +4,10 @@ import ( "bytes" "net/url" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/delegation" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/fil-forge/ucantone/ucan/promise" diff --git a/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_allocate_test.go b/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_allocate_test.go index 2d8414b3..250cda4f 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_allocate_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_allocate_test.go @@ -1,8 +1,8 @@ package rpc_test import ( - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/multiformats/go-multihash" "github.com/stretchr/testify/require" @@ -19,7 +19,7 @@ import ( func (s *RPCSuite) TestBlobAllocate_Basic() { t := s.T() - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) size := uint64(123) cause := testutil.RandomCID(t) space := testutil.RandomDID(t) @@ -59,7 +59,7 @@ func (s *RPCSuite) TestBlobAllocate_Basic() { // this at the validator. The handler check is the only guard. func (s *RPCSuite) TestBlobAllocate_SizeLimitExceeded() { t := s.T() - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) space := testutil.RandomDID(t) // One byte past the default cap. Costs nothing to run: only the declared diff --git a/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_reject_test.go b/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_reject_test.go index 63d4f570..83540ad2 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_reject_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_reject_test.go @@ -1,9 +1,9 @@ package rpc_test import ( - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/errors/datamodel" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan/delegation" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/fil-forge/ucantone/ucan/promise" diff --git a/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_release_test.go b/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_release_test.go index af11421e..48ba4e45 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_release_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/rpc/blob_release_test.go @@ -3,10 +3,10 @@ package rpc_test import ( "testing" - "github.com/fil-forge/libforge/commands/assert" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/protocol/commands/assert" + "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/testutil" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/delegation" "github.com/fil-forge/ucantone/ucan/invocation" @@ -297,7 +297,7 @@ func (s *RPCSuite) TestBlobRelease_InvalidCause_DigestMismatch() { digest := s.seedAcceptedBlob(t, space) // The remove names a different digest than the release. - remove := s.selfIssuedRemove(t, spaceSigner, testutil.RandomMultihash(t)) + remove := s.selfIssuedRemove(t, spaceSigner, testutil.RandomDigest(t)) rcpt := s.sendRelease(t, space, digest, remove.Task().Link(), []ucan.Invocation{remove}) assertReceiptFailure(t, rcpt, blob.InvalidCauseErrorName) s.assertClaimRetained(t, space, digest) diff --git a/piri/pkg/ucanhandlers/ucanfxtest/rpc/helpers_test.go b/piri/pkg/ucanhandlers/ucanfxtest/rpc/helpers_test.go index a5b57a2a..b0ca30b5 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/rpc/helpers_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/rpc/helpers_test.go @@ -4,8 +4,8 @@ import ( "bytes" "testing" - "github.com/fil-forge/libforge/commands/access" - "github.com/fil-forge/libforge/commands/blob" + "github.com/fil-forge/forge/protocol/commands/access" + "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/ucantone/errors/datamodel" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ucan" diff --git a/piri/pkg/ucanhandlers/ucanfxtest/rpc/suite_test.go b/piri/pkg/ucanhandlers/ucanfxtest/rpc/suite_test.go index d7decd6a..9b282309 100644 --- a/piri/pkg/ucanhandlers/ucanfxtest/rpc/suite_test.go +++ b/piri/pkg/ucanhandlers/ucanfxtest/rpc/suite_test.go @@ -4,11 +4,11 @@ import ( "net/url" "testing" - "github.com/fil-forge/libforge/identity" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/did/key" "github.com/fil-forge/ucantone/did/resolver" + "github.com/fil-forge/ucantone/testutil" "github.com/stretchr/testify/suite" "go.uber.org/fx" diff --git a/protocol/LICENSE.md b/protocol/LICENSE.md new file mode 100644 index 00000000..8b486090 --- /dev/null +++ b/protocol/LICENSE.md @@ -0,0 +1,232 @@ +The contents of this repository are Copyright (c) corresponding authors and +contributors, licensed under the `Permissive License Stack` meaning either of: + +- Apache-2.0 Software License: https://www.apache.org/licenses/LICENSE-2.0 + ([...4tr2kfsq](https://dweb.link/ipfs/bafkreiankqxazcae4onkp436wag2lj3ccso4nawxqkkfckd6cg4tr2kfsq)) + +- MIT Software License: https://opensource.org/licenses/MIT + ([...vljevcba](https://dweb.link/ipfs/bafkreiepofszg4gfe2gzuhojmksgemsub2h4uy2gewdnr35kswvljevcba)) + +You may not use the contents of this repository except in compliance +with one of the listed Licenses. For an extended clarification of the +intent behind the choice of Licensing please refer to +https://protocol.ai/blog/announcing-the-permissive-license-stack/ + +Unless required by applicable law or agreed to in writing, software +distributed under the terms listed in this notice is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See each License for the specific language +governing permissions and limitations under that License. + + + +`SPDX-License-Identifier: Apache-2.0 OR MIT` + +Verbatim copies of both licenses are included below: + +
Apache-2.0 Software License + +``` + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS +``` + +
+ +
MIT Software License + +``` +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +``` + +
diff --git a/protocol/Makefile b/protocol/Makefile new file mode 100644 index 00000000..f064c65f --- /dev/null +++ b/protocol/Makefile @@ -0,0 +1,26 @@ +.PHONY: gen build vet test codegen-build gen-check ci + +gen: + go generate ./... + +build: + go build ./... + +vet: + go vet ./... + +test: + go test ./... + +# Compile the generators under the tag they run with, so a generator that no +# longer compiles fails here rather than at the next `go generate`. +codegen-build: + go build -tags codegen -o $(shell mktemp -d) $(shell go list ./... | grep '/gen$$') + +# Regenerate and fail on any diff: catches stale generated files and generators +# that no longer compile. +gen-check: gen + @git diff --exit-code -- '*_gen.go' '*_gen.*.go' || \ + { echo "generated files are out of date; run 'make gen' and commit the result" >&2; exit 1; } + +ci: build vet codegen-build gen-check test diff --git a/protocol/blobindex/datamodel/cbor_gen.maps.go b/protocol/blobindex/datamodel/cbor_gen.maps.go new file mode 100644 index 00000000..46fc4225 --- /dev/null +++ b/protocol/blobindex/datamodel/cbor_gen.maps.go @@ -0,0 +1,265 @@ +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package datamodel + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *ShardedDagIndexModel) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 1 + + if t.DagO_1 == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.DagO_1 (datamodel.ShardedDagIndexModel_0_1) (struct) + if t.DagO_1 != nil { + + if len("index/sharded/dag@0.1") > 8192 { + return xerrors.Errorf("Value in field \"index/sharded/dag@0.1\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("index/sharded/dag@0.1"))); err != nil { + return err + } + if _, err := cw.WriteString(string("index/sharded/dag@0.1")); err != nil { + return err + } + + if err := t.DagO_1.MarshalCBOR(cw); err != nil { + return err + } + } + return nil +} + +func (t *ShardedDagIndexModel) UnmarshalCBOR(r io.Reader) (err error) { + *t = ShardedDagIndexModel{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ShardedDagIndexModel: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 21) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.DagO_1 (datamodel.ShardedDagIndexModel_0_1) (struct) + case "index/sharded/dag@0.1": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.DagO_1 = new(ShardedDagIndexModel_0_1) + if err := t.DagO_1.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.DagO_1 pointer: %w", err) + } + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ShardedDagIndexModel_0_1) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Shards ([]cid.Cid) (slice) + if len("shards") > 8192 { + return xerrors.Errorf("Value in field \"shards\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("shards"))); err != nil { + return err + } + if _, err := cw.WriteString(string("shards")); err != nil { + return err + } + + if len(t.Shards) > 8192 { + return xerrors.Errorf("Slice value in field t.Shards was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Shards))); err != nil { + return err + } + for _, v := range t.Shards { + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + return nil +} + +func (t *ShardedDagIndexModel_0_1) UnmarshalCBOR(r io.Reader) (err error) { + *t = ShardedDagIndexModel_0_1{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ShardedDagIndexModel_0_1: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Shards ([]cid.Cid) (slice) + case "shards": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Shards: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Shards = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Shards[i]: %w", err) + } + + t.Shards[i] = c + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/blobindex/datamodel/cbor_gen.tuples.go b/protocol/blobindex/datamodel/cbor_gen.tuples.go new file mode 100644 index 00000000..3e500112 --- /dev/null +++ b/protocol/blobindex/datamodel/cbor_gen.tuples.go @@ -0,0 +1,355 @@ +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package datamodel + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +var lengthBufRangeModel = []byte{130} + +func (t *RangeModel) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufRangeModel); err != nil { + return err + } + + // t.Start (int64) (int64) + if t.Start >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Start)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Start-1)); err != nil { + return err + } + } + + // t.End (int64) (int64) + if t.End >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.End)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.End-1)); err != nil { + return err + } + } + + return nil +} + +func (t *RangeModel) UnmarshalCBOR(r io.Reader) (err error) { + *t = RangeModel{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 2 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Start (int64) (int64) + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.Start = int64(extraI) + } + // t.End (int64) (int64) + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.End = int64(extraI) + } + return nil +} + +var lengthBufBlobSliceModel = []byte{130} + +func (t *BlobSliceModel) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufBlobSliceModel); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + // t.Range (datamodel.RangeModel) (struct) + if err := t.Range.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *BlobSliceModel) UnmarshalCBOR(r io.Reader) (err error) { + *t = BlobSliceModel{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 2 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Digest (multihash.Multihash) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + // t.Range (datamodel.RangeModel) (struct) + + { + + if err := t.Range.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Range: %w", err) + } + + } + return nil +} + +var lengthBufBlobIndexModel = []byte{130} + +func (t *BlobIndexModel) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufBlobIndexModel); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + // t.Slices ([]datamodel.BlobSliceModel) (slice) + if len(t.Slices) > 8192 { + return xerrors.Errorf("Slice value in field t.Slices was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Slices))); err != nil { + return err + } + for _, v := range t.Slices { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *BlobIndexModel) UnmarshalCBOR(r io.Reader) (err error) { + *t = BlobIndexModel{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 2 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Digest (multihash.Multihash) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + // t.Slices ([]datamodel.BlobSliceModel) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Slices: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Slices = make([]BlobSliceModel, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Slices[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Slices[i]: %w", err) + } + + } + + } + } + return nil +} diff --git a/protocol/blobindex/datamodel/gen/main.go b/protocol/blobindex/datamodel/gen/main.go new file mode 100644 index 00000000..1891b4d2 --- /dev/null +++ b/protocol/blobindex/datamodel/gen/main.go @@ -0,0 +1,37 @@ +//go:generate go run . + +package main + +import ( + jsg "github.com/alanshaw/dag-json-gen" + dm "github.com/fil-forge/forge/protocol/blobindex/datamodel" + cbg "github.com/whyrusleeping/cbor-gen" +) + +func main() { + mapModels := []any{ + dm.ShardedDagIndexModel{}, + dm.ShardedDagIndexModel_0_1{}, + } + tupleModels := []any{ + dm.RangeModel{}, + dm.BlobSliceModel{}, + dm.BlobIndexModel{}, + } + + if err := cbg.WriteTupleEncodersToFile("../cbor_gen.tuples.go", "datamodel", tupleModels...); err != nil { + panic(err) + } + + if err := cbg.WriteMapEncodersToFile("../cbor_gen.maps.go", "datamodel", mapModels...); err != nil { + panic(err) + } + + if err := jsg.WriteTupleEncodersToFile("../json_gen.tuples.go", "datamodel", tupleModels...); err != nil { + panic(err) + } + + if err := jsg.WriteMapEncodersToFile("../json_gen.maps.go", "datamodel", mapModels...); err != nil { + panic(err) + } +} diff --git a/protocol/blobindex/datamodel/json_gen.maps.go b/protocol/blobindex/datamodel/json_gen.maps.go new file mode 100644 index 00000000..78810914 --- /dev/null +++ b/protocol/blobindex/datamodel/json_gen.maps.go @@ -0,0 +1,275 @@ +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package datamodel + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *ShardedDagIndexModel) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.DagO_1 (datamodel.ShardedDagIndexModel_0_1) (struct) + if t.DagO_1 != nil { + if len("index/sharded/dag@0.1") > 8192 { + return fmt.Errorf("string in field \"index/sharded/dag@0.1\" was too long") + } + if err := jw.WriteString(string("index/sharded/dag@0.1")); err != nil { + return fmt.Errorf("writing string for field \"index/sharded/dag@0.1\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.DagO_1.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.DagO_1: %w", err) + } + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ShardedDagIndexModel) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ShardedDagIndexModel{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ShardedDagIndexModel: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ShardedDagIndexModel: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ShardedDagIndexModel: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ShardedDagIndexModel: string too large") + } + return fmt.Errorf("reading string for field ShardedDagIndexModel: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ShardedDagIndexModel: %w", err) + } + switch name { + + // t.DagO_1 (datamodel.ShardedDagIndexModel_0_1) (struct) + case "index/sharded/dag@0.1": + + { + null, err := jr.PeekNull() + if err != nil { + return fmt.Errorf("peeking null for field t.DagO_1: %w", err) + } + if null { + if err := jr.ReadNull(); err != nil { + return fmt.Errorf("reading null for field t.DagO_1: %w", err) + } + } else { + t.DagO_1 = new(ShardedDagIndexModel_0_1) + if err := t.DagO_1.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.DagO_1 pointer: %w", err) + } + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ShardedDagIndexModel: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ShardedDagIndexModel: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ShardedDagIndexModel") + } + } + } + + return nil +} +func (t *ShardedDagIndexModel_0_1) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Shards ([]cid.Cid) (slice) + if len("shards") > 8192 { + return fmt.Errorf("string in field \"shards\" was too long") + } + if err := jw.WriteString(string("shards")); err != nil { + return fmt.Errorf("writing string for field \"shards\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Shards) > 8192 { + return fmt.Errorf("slice value in field t.Shards was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Shards: %w", err) + } + for i, v := range t.Shards { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Shards: %w", err) + } + } + + if err := jw.WriteCid(v); err != nil { + return fmt.Errorf("writing CID for field v: %w", err) + } + + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Shards: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ShardedDagIndexModel_0_1) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ShardedDagIndexModel_0_1{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ShardedDagIndexModel_0_1: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ShardedDagIndexModel_0_1: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ShardedDagIndexModel_0_1: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ShardedDagIndexModel_0_1: string too large") + } + return fmt.Errorf("reading string for field ShardedDagIndexModel_0_1: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ShardedDagIndexModel_0_1: %w", err) + } + switch name { + + // t.Shards ([]cid.Cid) (slice) + case "shards": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Shards: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Shards: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Shards: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]cid.Cid, 1) + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field item[0]: %w", err) + } + item[0] = c + + } + t.Shards = append(t.Shards, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Shards: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Shards: slice too large") + } + } + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ShardedDagIndexModel_0_1: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ShardedDagIndexModel_0_1: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ShardedDagIndexModel_0_1") + } + } + } + + return nil +} diff --git a/protocol/blobindex/datamodel/json_gen.tuples.go b/protocol/blobindex/datamodel/json_gen.tuples.go new file mode 100644 index 00000000..3406b90e --- /dev/null +++ b/protocol/blobindex/datamodel/json_gen.tuples.go @@ -0,0 +1,351 @@ +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package datamodel + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *RangeModel) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field RangeModel: %w", err) + } + + // t.Start (int64) (int64) + + if err := jw.WriteInt64(int64(t.Start)); err != nil { + return fmt.Errorf("writing int64 for field t.Start: %w", err) + } + + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field End: %w", err) + } + + // t.End (int64) (int64) + + if err := jw.WriteInt64(int64(t.End)); err != nil { + return fmt.Errorf("writing int64 for field t.End: %w", err) + } + + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field RangeModel: %w", err) + } + return nil +} + +func (t *RangeModel) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RangeModel{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field RangeModel: %w", err) + } + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field RangeModel: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field RangeModel: %w", err) + } + } else { + + // t.Start (int64) (int64) + + { + + nval, err := jr.ReadNumberAsInt64() + if err != nil { + return fmt.Errorf("reading int64 for field t.Start: %w", err) + } + t.Start = int64(nval) + + } + { + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field RangeModel: %w", err) + } + if close { + return fmt.Errorf("json input has too few fields 1 < 2") + } + } + + // t.End (int64) (int64) + + { + + nval, err := jr.ReadNumberAsInt64() + if err != nil { + return fmt.Errorf("reading int64 for field t.End: %w", err) + } + t.End = int64(nval) + + } + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field RangeModel: %w", err) + } + } + return nil +} + +func (t *BlobSliceModel) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field BlobSliceModel: %w", err) + } + + // t.Digest (multihash.Multihash) (slice) + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field Range: %w", err) + } + + // t.Range (datamodel.RangeModel) (struct) + if err := t.Range.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Range: %w", err) + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field BlobSliceModel: %w", err) + } + return nil +} + +func (t *BlobSliceModel) UnmarshalDagJSON(r io.Reader) (err error) { + *t = BlobSliceModel{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field BlobSliceModel: %w", err) + } + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field BlobSliceModel: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field BlobSliceModel: %w", err) + } + } else { + + // t.Digest (multihash.Multihash) (slice) + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + { + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field BlobSliceModel: %w", err) + } + if close { + return fmt.Errorf("json input has too few fields 1 < 2") + } + } + + // t.Range (datamodel.RangeModel) (struct) + + if err := t.Range.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Range: %w", err) + } + + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field BlobSliceModel: %w", err) + } + } + return nil +} + +func (t *BlobIndexModel) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field BlobIndexModel: %w", err) + } + + // t.Digest (multihash.Multihash) (slice) + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field Slices: %w", err) + } + + // t.Slices ([]datamodel.BlobSliceModel) (slice) + if len(t.Slices) > 8192 { + return fmt.Errorf("slice value in field t.Slices was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Slices: %w", err) + } + for i, v := range t.Slices { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Slices: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Slices: %w", err) + } + + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field BlobIndexModel: %w", err) + } + return nil +} + +func (t *BlobIndexModel) UnmarshalDagJSON(r io.Reader) (err error) { + *t = BlobIndexModel{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field BlobIndexModel: %w", err) + } + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field BlobIndexModel: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field BlobIndexModel: %w", err) + } + } else { + + // t.Digest (multihash.Multihash) (slice) + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + { + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field BlobIndexModel: %w", err) + } + if close { + return fmt.Errorf("json input has too few fields 1 < 2") + } + } + + // t.Slices ([]datamodel.BlobSliceModel) (slice) + + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Slices: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Slices: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Slices: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]BlobSliceModel, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Slices = append(t.Slices, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Slices: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Slices: slice too large") + } + } + } + + } + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field BlobIndexModel: %w", err) + } + } + return nil +} diff --git a/protocol/blobindex/datamodel/shardeddagindex.go b/protocol/blobindex/datamodel/shardeddagindex.go new file mode 100644 index 00000000..f97325de --- /dev/null +++ b/protocol/blobindex/datamodel/shardeddagindex.go @@ -0,0 +1,34 @@ +package datamodel + +import ( + cid "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" +) + +// ShardedDagIndexModel is the golang structure for encoding sharded DAG index header blocks +type ShardedDagIndexModel struct { + DagO_1 *ShardedDagIndexModel_0_1 `cborgen:"index/sharded/dag@0.1,omitempty" dagjsongen:"index/sharded/dag@0.1,omitempty"` +} + +// ShardedDagIndexModel_0_1 describes the 0.1 version of ShardedDagIndex +type ShardedDagIndexModel_0_1 struct { + Shards []cid.Cid `cborgen:"shards" dagjsongen:"shards"` +} + +// RangeModel is a start and end byte offset for a slice in a blob (inclusive) +type RangeModel struct { + Start int64 + End int64 +} + +// BlobSliceModel describes a multihash and its byte offset in a blob +type BlobSliceModel struct { + Digest multihash.Multihash + Range RangeModel +} + +// BlobIndexModel is the golang structure for encoding a shard of CIDs in a block +type BlobIndexModel struct { + Digest multihash.Multihash + Slices []BlobSliceModel +} diff --git a/protocol/blobindex/multihashmap.go b/protocol/blobindex/multihashmap.go new file mode 100644 index 00000000..dc84543e --- /dev/null +++ b/protocol/blobindex/multihashmap.go @@ -0,0 +1,11 @@ +package blobindex + +import ( + "github.com/fil-forge/forge/protocol/bytemap" + mh "github.com/multiformats/go-multihash" +) + +// NewMultihashMap returns a new map of multihash to a data type +func NewMultihashMap[T any](sizeHint int) MultihashMap[T] { + return bytemap.NewByteMap[mh.Multihash, T](sizeHint) +} diff --git a/protocol/blobindex/shardeddagindex.go b/protocol/blobindex/shardeddagindex.go new file mode 100644 index 00000000..49289d1a --- /dev/null +++ b/protocol/blobindex/shardeddagindex.go @@ -0,0 +1,231 @@ +package blobindex + +import ( + "bytes" + "fmt" + "io" + "slices" + + "github.com/fil-forge/automobile" + dm "github.com/fil-forge/forge/protocol/blobindex/datamodel" + "github.com/ipfs/go-cid" + mh "github.com/multiformats/go-multihash" +) + +// Extract extracts a sharded dag index from a car +func Extract(r io.Reader) (*MapShardedDagIndex, error) { + dc, err := decodeIndexCar(r) + if err != nil { + return nil, NewDecodeFailureError(err) + } + return View(dc.root, dc.blocks) +} + +// indexCar is the decoded view of an index CAR file. +type indexCar struct { + root cid.Cid + blocks map[cid.Cid][]byte +} + +func decodeIndexCar(r io.Reader) (indexCar, error) { + roots, blocks, err := automobile.Decode(r) + if err != nil { + return indexCar{}, fmt.Errorf("decoding index CAR: %w", err) + } + if len(roots) != 1 { + return indexCar{}, fmt.Errorf("expected exactly one root, got: %d", len(roots)) + } + codec := roots[0].Prefix().Codec + if codec != cid.DagCBOR { + return indexCar{}, fmt.Errorf("unexpected root CID codec: %x", codec) + } + data := indexCar{root: roots[0], blocks: map[cid.Cid][]byte{}} + for _, blk := range blocks { + data.blocks[blk.Link] = blk.Data + } + if _, ok := data.blocks[data.root]; !ok { + return indexCar{}, fmt.Errorf("missing root block: %s", data.root) + } + return data, nil +} + +func View(root cid.Cid, blocks map[cid.Cid][]byte) (*MapShardedDagIndex, error) { + rootBlock, ok := blocks[root] + if !ok { + return nil, NewDecodeFailureError(fmt.Errorf("missing root block: %s", root)) + } + + var shardedDagIndexData dm.ShardedDagIndexModel + if err := shardedDagIndexData.UnmarshalCBOR(bytes.NewReader(rootBlock)); err != nil { + return nil, NewDecodeFailureError(fmt.Errorf("decoding root block: %s: %v", root, err)) + } + if shardedDagIndexData.DagO_1 == nil { + return nil, NewUnknownFormatError(fmt.Errorf("unknown index version")) + } + + dagIndex := NewShardedDagIndex(len(shardedDagIndexData.DagO_1.Shards)) + for _, shardLink := range shardedDagIndexData.DagO_1.Shards { + shard, ok := blocks[shardLink] + if !ok { + return nil, NewDecodeFailureError(fmt.Errorf("missing shard block: %s", shardLink)) + } + var blobIndexData dm.BlobIndexModel + err := blobIndexData.UnmarshalCBOR(bytes.NewReader(shard)) + if err != nil { + return nil, NewDecodeFailureError(err) + } + blobIndex := NewMultihashMap[Range](len(blobIndexData.Slices)) + for _, blobSlice := range blobIndexData.Slices { + blobIndex.Set(blobSlice.Digest, blobSlice.Range) + } + dagIndex.Shards().Set(blobIndexData.Digest, blobIndex) + } + return dagIndex, nil +} + +// MapShardedDagIndex is an in-memory implementation of ShardedDagIndex +// using [MultihashMap]. +type MapShardedDagIndex struct { + shards MultihashMap[MultihashMap[Range]] +} + +// NewShardedDagIndex constructs an empty sharded DAG index. +// shardSizeHint is used to preallocate the number of shards that will be added. +// Set to -1 if unknown. +func NewShardedDagIndex(shardSizeHint int) *MapShardedDagIndex { + return &MapShardedDagIndex{NewMultihashMap[MultihashMap[Range]](shardSizeHint)} +} + +func (sdi *MapShardedDagIndex) Shards() MultihashMap[MultihashMap[Range]] { + return sdi.shards +} + +func (sdi *MapShardedDagIndex) SetSlice(shard mh.Multihash, slice mh.Multihash, byteRange Range) { + index := sdi.shards.Get(shard) + if index == nil { + index = NewMultihashMap[Range](-1) + sdi.shards.Set(shard, index) + } + index.Set(slice, byteRange) +} + +func (sdi *MapShardedDagIndex) Archive(w io.Writer) error { + return Archive(sdi, w) +} + +// NewUnknownFormatError returns an error for an unknown format. +func NewUnknownFormatError(reason error) error { + return fmt.Errorf("unknown format: %w", reason) +} + +// NewDecodeFailureError returns an error for a decode failure. +func NewDecodeFailureError(reason error) error { + return fmt.Errorf("decode failure: %w", reason) +} + +// Archive writes a ShardedDagIndex to a CAR file +func Archive(index ShardedDagIndex, writer io.Writer) error { + // assemble blob index shards + blobIndexDatas, err := toList(index.Shards(), func(shardHash mh.Multihash, shard MultihashMap[Range]) (dm.BlobIndexModel, error) { + // assemble blob slices + blobSliceDatas, err := toList(shard, func(sliceHash mh.Multihash, byteRange Range) (dm.BlobSliceModel, error) { + return dm.BlobSliceModel{Digest: sliceHash, Range: byteRange}, nil + }) + if err != nil { + return dm.BlobIndexModel{}, err + } + // sort blob slices + if err := sortByDigest(blobSliceDatas, func(bsm dm.BlobSliceModel) mh.Multihash { + return bsm.Digest + }); err != nil { + return dm.BlobIndexModel{}, err + } + return dm.BlobIndexModel{ + Digest: shardHash, + Slices: blobSliceDatas, + }, nil + }) + if err != nil { + return err + } + // sort blob index shards + if err := sortByDigest(blobIndexDatas, func(bim dm.BlobIndexModel) mh.Multihash { + return bim.Digest + }); err != nil { + return err + } + + // initialize root sharded dag index + shardedDagIndex := dm.ShardedDagIndexModel_0_1{ + Shards: make([]cid.Cid, 0, len(blobIndexDatas)), + } + // encode blob index shards to blocks and add links to sharded dag index + blks := make([]automobile.Block, 0, len(blobIndexDatas)+1) + for _, shard := range blobIndexDatas { + var buf bytes.Buffer + err := shard.MarshalCBOR(&buf) + if err != nil { + return err + } + b := buf.Bytes() + l, err := cid.V1Builder{Codec: cid.DagCBOR, MhType: mh.SHA2_256}.Sum(b) + if err != nil { + return err + } + blks = append(blks, automobile.Block{Data: b, Link: l}) + shardedDagIndex.Shards = append(shardedDagIndex.Shards, l) + } + + // encode the root block + model := dm.ShardedDagIndexModel{DagO_1: &shardedDagIndex} + var rootData bytes.Buffer + if err := model.MarshalCBOR(&rootData); err != nil { + return err + } + root, err := cid.V1Builder{Codec: cid.DagCBOR, MhType: mh.SHA2_256}.Sum(rootData.Bytes()) + if err != nil { + return err + } + rootBlock := automobile.Block{Link: root, Data: rootData.Bytes()} + + carWriter := automobile.NewWriter(writer) + if err := carWriter.WriteHeader([]cid.Cid{root}); err != nil { + return fmt.Errorf("writing CAR header: %w", err) + } + for _, blk := range append(blks, rootBlock) { + if err := carWriter.WriteBlock(blk); err != nil { + return fmt.Errorf("writing CAR block: %w", err) + } + } + return nil +} + +func toList[E, T any](mhm MultihashMap[T], newElem func(mh.Multihash, T) (E, error)) ([]E, error) { + asList := make([]E, 0, mhm.Size()) + for hash, value := range mhm.Iterator() { + e, err := newElem(hash, value) + if err != nil { + return nil, err + } + asList = append(asList, e) + } + return asList, nil +} + +func sortByDigest[E any](list []E, getDigest func(E) mh.Multihash) error { + decodeds := NewMultihashMap[*mh.DecodedMultihash](len(list)) + for _, e := range list { + hash := getDigest(e) + decoded, err := mh.Decode(hash) + if err != nil { + return err + } + decodeds.Set(hash, decoded) + } + slices.SortFunc(list, func(a, b E) int { + decodedA := decodeds.Get(getDigest(a)) + decodedB := decodeds.Get(getDigest(b)) + return bytes.Compare(decodedA.Digest, decodedB.Digest) + }) + return nil +} diff --git a/protocol/blobindex/shardeddagindex_test.go b/protocol/blobindex/shardeddagindex_test.go new file mode 100644 index 00000000..4d408412 --- /dev/null +++ b/protocol/blobindex/shardeddagindex_test.go @@ -0,0 +1,113 @@ +package blobindex_test + +import ( + "bytes" + "math/rand/v2" + "testing" + + "github.com/fil-forge/forge/protocol/blobindex" + "github.com/fil-forge/ucantone/testutil" + "github.com/ipfs/go-cid" + multihash "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +func TestFromToArchive(t *testing.T) { + testIndexData := map[cid.Cid][]struct { + digest multihash.Multihash + byteRange []int + }{ + testutil.RandomCID(t): { + {digest: testutil.RandomDigest(t), byteRange: []int{0, 99}}, + {digest: testutil.RandomDigest(t), byteRange: []int{100, 199}}, + {digest: testutil.RandomDigest(t), byteRange: []int{200, 299}}, + }, + testutil.RandomCID(t): { + {digest: testutil.RandomDigest(t), byteRange: []int{0, 34}}, + {digest: testutil.RandomDigest(t), byteRange: []int{35, 58}}, + }, + } + + index := blobindex.NewShardedDagIndex(len(testIndexData)) + for shard, slices := range testIndexData { + for _, slice := range slices { + index.SetSlice( + shard.Hash(), + slice.digest, + blobindex.Range{ + Start: int64(slice.byteRange[0]), + End: int64(slice.byteRange[1]), + }, + ) + } + } + + // ensure all the data is in the index + require.Equal(t, len(testIndexData), index.Shards().Size()) + for shard, slices := range testIndexData { + require.True(t, index.Shards().Has(shard.Hash())) + + idxSlices := index.Shards().Get(shard.Hash()) + require.Equal(t, len(slices), idxSlices.Size()) + + for _, slice := range slices { + require.True(t, idxSlices.Has(slice.digest)) + byteRange := idxSlices.Get(slice.digest) + require.Equal(t, int64(slice.byteRange[0]), byteRange.Start) + require.Equal(t, int64(slice.byteRange[1]), byteRange.End) + } + } + + t.Run("round trip", func(t *testing.T) { + var buf bytes.Buffer + err := index.Archive(&buf) + require.NoError(t, err) + + newIndex, err := blobindex.Extract(&buf) + require.NoError(t, err) + + // ensure all the data is in the new index + require.Equal(t, index.Shards().Size(), newIndex.Shards().Size()) + for shard, slices := range index.Shards().Iterator() { + require.True(t, newIndex.Shards().Has(shard)) + + newSlices := newIndex.Shards().Get(shard) + require.Equal(t, slices.Size(), newSlices.Size()) + + for slice, byteRange := range slices.Iterator() { + require.True(t, newSlices.Has(slice)) + newByteRange := newSlices.Get(slice) + require.Equal(t, byteRange.Start, newByteRange.Start) + require.Equal(t, byteRange.End, newByteRange.End) + } + } + }) + + t.Run("deterministic", func(t *testing.T) { + index1 := blobindex.NewShardedDagIndex(len(testIndexData)) + for shard, slices := range testIndexData { + // shuffle the order slices are added + rand.Shuffle(len(slices), func(i, j int) { + slices[i], slices[j] = slices[j], slices[i] + }) + for _, slice := range slices { + index1.SetSlice( + shard.Hash(), + slice.digest, + blobindex.Range{ + Start: int64(slice.byteRange[0]), + End: int64(slice.byteRange[1]), + }, + ) + } + } + + var buf1, buf2 bytes.Buffer + err := index.Archive(&buf1) + require.NoError(t, err) + err = index1.Archive(&buf2) + require.NoError(t, err) + + require.Equal(t, buf1.Bytes(), buf2.Bytes()) + }) +} diff --git a/protocol/blobindex/types.go b/protocol/blobindex/types.go new file mode 100644 index 00000000..14386a7d --- /dev/null +++ b/protocol/blobindex/types.go @@ -0,0 +1,21 @@ +package blobindex + +import ( + dm "github.com/fil-forge/forge/protocol/blobindex/datamodel" + "github.com/fil-forge/forge/protocol/bytemap" + mh "github.com/multiformats/go-multihash" +) + +// MultihashMap is a map for mapping multihash digests to arbitrary data types. +type MultihashMap[T any] interface { + bytemap.ByteMap[mh.Multihash, T] +} + +// Range describes a start and end byte offset within a shard (inclusive). +type Range = dm.RangeModel + +// ShardedDagIndex is a blob index for a DAG stored over one or more shards. +type ShardedDagIndex interface { + // Index information for shards the DAG is split across. + Shards() MultihashMap[MultihashMap[Range]] +} diff --git a/protocol/bytemap/bytemap.go b/protocol/bytemap/bytemap.go new file mode 100644 index 00000000..0d00fcd5 --- /dev/null +++ b/protocol/bytemap/bytemap.go @@ -0,0 +1,80 @@ +package bytemap + +import ( + "iter" + "maps" +) + +type byteMap[K ~[]byte, T any] struct { + data map[string]T +} + +// ByteMap is a generic for mapping byte array like types to arbitrary data types +type ByteMap[K ~[]byte, T any] interface { + Get(K) T + Has(K) bool + Set(K, T) + Delete(K) bool + Size() int + Iterator() iter.Seq2[K, T] + Keys() iter.Seq[K] + Values() iter.Seq[T] +} + +// NewByteMap returns a new map of multihash to a data type +func NewByteMap[K ~[]byte, T any](sizeHint int) ByteMap[K, T] { + var stringMap map[string]T + if sizeHint == -1 { + stringMap = make(map[string]T) + } else { + stringMap = make(map[string]T, sizeHint) + } + return &byteMap[K, T]{stringMap} +} + +func (bm *byteMap[K, T]) Get(b K) T { + return bm.data[string(b)] +} + +func (bm *byteMap[K, T]) Has(b K) bool { + _, ok := bm.data[string(b)] + return ok +} + +func (bm *byteMap[K, T]) Set(b K, t T) { + bm.data[string(b)] = t +} + +func (bm *byteMap[K, T]) Delete(b K) bool { + _, ok := bm.data[string(b)] + delete(bm.data, string(b)) + return ok +} + +func (bm *byteMap[K, T]) Size() int { + return len(bm.data) +} + +func (bm *byteMap[K, T]) Iterator() iter.Seq2[K, T] { + return func(yield func(K, T) bool) { + for k, v := range bm.data { + if !yield(K(k), v) { + return + } + } + } +} + +func (bm *byteMap[K, T]) Keys() iter.Seq[K] { + return func(yield func(K) bool) { + for k := range bm.data { + if !yield(K(k)) { + return + } + } + } +} + +func (bm *byteMap[K, T]) Values() iter.Seq[T] { + return maps.Values(bm.data) +} diff --git a/protocol/bytemap/bytemap_test.go b/protocol/bytemap/bytemap_test.go new file mode 100644 index 00000000..87959978 --- /dev/null +++ b/protocol/bytemap/bytemap_test.go @@ -0,0 +1,48 @@ +package bytemap_test + +import ( + "testing" + + "github.com/fil-forge/forge/protocol/bytemap" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" +) + +func TestKeys(t *testing.T) { + bm := bytemap.NewByteMap[multihash.Multihash, struct{}](-1) + + d0, err := multihash.Sum([]byte{1, 2, 3}, multihash.SHA2_256, -1) + require.NoError(t, err) + d1, err := multihash.Sum([]byte{4, 5, 6}, multihash.SHA2_256, -1) + require.NoError(t, err) + d2, err := multihash.Sum([]byte{7, 8, 9}, multihash.SHA2_256, -1) + require.NoError(t, err) + + digests := []multihash.Multihash{d0, d1, d2} + for _, d := range digests { + bm.Set(d, struct{}{}) + } + + var keys []multihash.Multihash + for d := range bm.Keys() { + keys = append(keys, d) + } + + require.ElementsMatch(t, digests, keys) +} + +func TestValues(t *testing.T) { + bm := bytemap.NewByteMap[multihash.Multihash, string](-1) + + strings := []string{"this", "is", "fine"} + for _, v := range strings { + bm.Set([]byte("key"+v), v) + } + + var values []string + for d := range bm.Values() { + values = append(values, d) + } + + require.ElementsMatch(t, strings, values) +} diff --git a/protocol/commands/access/cbor_gen.go b/protocol/commands/access/cbor_gen.go new file mode 100644 index 00000000..e4fd8586 --- /dev/null +++ b/protocol/commands/access/cbor_gen.go @@ -0,0 +1,1108 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package access + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *RequestArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Attenuations ([]access.CapabilityRequest) (slice) + if len("att") > 8192 { + return xerrors.Errorf("Value in field \"att\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("att"))); err != nil { + return err + } + if _, err := cw.WriteString(string("att")); err != nil { + return err + } + + if len(t.Attenuations) > 8192 { + return xerrors.Errorf("Slice value in field t.Attenuations was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Attenuations))); err != nil { + return err + } + for _, v := range t.Attenuations { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + + // t.Issuer (did.DID) (struct) + if len("iss") > 8192 { + return xerrors.Errorf("Value in field \"iss\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("iss"))); err != nil { + return err + } + if _, err := cw.WriteString(string("iss")); err != nil { + return err + } + + if err := t.Issuer.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *RequestArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RequestArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RequestArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 3) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Attenuations ([]access.CapabilityRequest) (slice) + case "att": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Attenuations: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Attenuations = make([]CapabilityRequest, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Attenuations[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Attenuations[i]: %w", err) + } + + } + + } + } + // t.Issuer (did.DID) (struct) + case "iss": + + { + + if err := t.Issuer.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Issuer: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *CapabilityRequest) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Command (command.Command) (struct) + if len("cmd") > 8192 { + return xerrors.Errorf("Value in field \"cmd\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cmd"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cmd")); err != nil { + return err + } + + if err := t.Command.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *CapabilityRequest) UnmarshalCBOR(r io.Reader) (err error) { + *t = CapabilityRequest{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("CapabilityRequest: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 3) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Command (command.Command) (struct) + case "cmd": + + { + + if err := t.Command.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Command: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RequestOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.Expiration (int64) (int64) + if len("exp") > 8192 { + return xerrors.Errorf("Value in field \"exp\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("exp"))); err != nil { + return err + } + if _, err := cw.WriteString(string("exp")); err != nil { + return err + } + + if t.Expiration >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Expiration)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Expiration-1)); err != nil { + return err + } + } + + // t.Request (cid.Cid) (struct) + if len("req") > 8192 { + return xerrors.Errorf("Value in field \"req\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("req"))); err != nil { + return err + } + if _, err := cw.WriteString(string("req")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Request); err != nil { + return xerrors.Errorf("failed to write cid field t.Request: %w", err) + } + + // t.Confirm (promise.AwaitOK) (struct) + if len("confirm") > 8192 { + return xerrors.Errorf("Value in field \"confirm\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("confirm"))); err != nil { + return err + } + if _, err := cw.WriteString(string("confirm")); err != nil { + return err + } + + if err := t.Confirm.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *RequestOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = RequestOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RequestOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Expiration (int64) (int64) + case "exp": + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.Expiration = int64(extraI) + } + // t.Request (cid.Cid) (struct) + case "req": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Request: %w", err) + } + + t.Request = c + + } + // t.Confirm (promise.AwaitOK) (struct) + case "confirm": + + { + + if err := t.Confirm.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Confirm: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ClaimOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Delegations ([]cid.Cid) (slice) + if len("delegations") > 8192 { + return xerrors.Errorf("Value in field \"delegations\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("delegations"))); err != nil { + return err + } + if _, err := cw.WriteString(string("delegations")); err != nil { + return err + } + + if len(t.Delegations) > 8192 { + return xerrors.Errorf("Slice value in field t.Delegations was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Delegations))); err != nil { + return err + } + for _, v := range t.Delegations { + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + return nil +} + +func (t *ClaimOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = ClaimOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ClaimOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Delegations ([]cid.Cid) (slice) + case "delegations": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Delegations: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Delegations = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Delegations[i]: %w", err) + } + + t.Delegations[i] = c + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ConfirmArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{164}); err != nil { + return err + } + + // t.Attenuations ([]access.CapabilityRequest) (slice) + if len("att") > 8192 { + return xerrors.Errorf("Value in field \"att\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("att"))); err != nil { + return err + } + if _, err := cw.WriteString(string("att")); err != nil { + return err + } + + if len(t.Attenuations) > 8192 { + return xerrors.Errorf("Slice value in field t.Attenuations was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Attenuations))); err != nil { + return err + } + for _, v := range t.Attenuations { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + + // t.Audience (did.DID) (struct) + if len("aud") > 8192 { + return xerrors.Errorf("Value in field \"aud\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("aud"))); err != nil { + return err + } + if _, err := cw.WriteString(string("aud")); err != nil { + return err + } + + if err := t.Audience.MarshalCBOR(cw); err != nil { + return err + } + + // t.Issuer (did.DID) (struct) + if len("iss") > 8192 { + return xerrors.Errorf("Value in field \"iss\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("iss"))); err != nil { + return err + } + if _, err := cw.WriteString(string("iss")); err != nil { + return err + } + + if err := t.Issuer.MarshalCBOR(cw); err != nil { + return err + } + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return xerrors.Errorf("Value in field \"cause\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cause"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cause")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Cause); err != nil { + return xerrors.Errorf("failed to write cid field t.Cause: %w", err) + } + + return nil +} + +func (t *ConfirmArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ConfirmArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ConfirmArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Attenuations ([]access.CapabilityRequest) (slice) + case "att": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Attenuations: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Attenuations = make([]CapabilityRequest, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Attenuations[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Attenuations[i]: %w", err) + } + + } + + } + } + // t.Audience (did.DID) (struct) + case "aud": + + { + + if err := t.Audience.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Audience: %w", err) + } + + } + // t.Issuer (did.DID) (struct) + case "iss": + + { + + if err := t.Issuer.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Issuer: %w", err) + } + + } + // t.Cause (cid.Cid) (struct) + case "cause": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Cause: %w", err) + } + + t.Cause = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *DelegateArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Delegations ([]cid.Cid) (slice) + if len("delegations") > 8192 { + return xerrors.Errorf("Value in field \"delegations\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("delegations"))); err != nil { + return err + } + if _, err := cw.WriteString(string("delegations")); err != nil { + return err + } + + if len(t.Delegations) > 8192 { + return xerrors.Errorf("Slice value in field t.Delegations was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Delegations))); err != nil { + return err + } + for _, v := range t.Delegations { + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + return nil +} + +func (t *DelegateArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = DelegateArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("DelegateArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Delegations ([]cid.Cid) (slice) + case "delegations": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Delegations: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Delegations = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Delegations[i]: %w", err) + } + + t.Delegations[i] = c + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *GrantArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 2 + + if t.Cause == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Attenuations ([]access.CapabilityRequest) (slice) + if len("att") > 8192 { + return xerrors.Errorf("Value in field \"att\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("att"))); err != nil { + return err + } + if _, err := cw.WriteString(string("att")); err != nil { + return err + } + + if len(t.Attenuations) > 8192 { + return xerrors.Errorf("Slice value in field t.Attenuations was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Attenuations))); err != nil { + return err + } + for _, v := range t.Attenuations { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + + // t.Cause (cid.Cid) (struct) + if t.Cause != nil { + + if len("cause") > 8192 { + return xerrors.Errorf("Value in field \"cause\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cause"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cause")); err != nil { + return err + } + + if t.Cause == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cbg.WriteCid(cw, *t.Cause); err != nil { + return xerrors.Errorf("failed to write cid field t.Cause: %w", err) + } + } + + } + return nil +} + +func (t *GrantArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = GrantArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("GrantArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Attenuations ([]access.CapabilityRequest) (slice) + case "att": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Attenuations: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Attenuations = make([]CapabilityRequest, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Attenuations[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Attenuations[i]: %w", err) + } + + } + + } + } + // t.Cause (cid.Cid) (struct) + case "cause": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Cause: %w", err) + } + + t.Cause = &c + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/access/claim.go b/protocol/commands/access/claim.go new file mode 100644 index 00000000..93371de2 --- /dev/null +++ b/protocol/commands/access/claim.go @@ -0,0 +1,15 @@ +//go:build !codegen + +package access + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + command "github.com/fil-forge/ucantone/ucan/command" +) + +type ClaimArguments = commands.Unit + +// Claim can be invoked by an agent to claim a set of delegations from the +// account. +var Claim = binding.Bind[*ClaimArguments, *ClaimOK](command.MustParse("/access/claim")) diff --git a/protocol/commands/access/confirm.go b/protocol/commands/access/confirm.go new file mode 100644 index 00000000..71caaaed --- /dev/null +++ b/protocol/commands/access/confirm.go @@ -0,0 +1,31 @@ +//go:build !codegen + +package access + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + command "github.com/fil-forge/ucantone/ucan/command" +) + +// ConfirmOK mirrors ClaimOK — confirming an access request grants the same +// shape of delegations bundle as claiming them. +type ConfirmOK = ClaimOK + +// ConfirmMetaKey is the key in metadata in any delegation created by a +// successful access request. The value is a link back to the `/access/confirm` +// invocation. +const ConfirmMetaKey = "accessConfirm" + +// Confirm can be invoked by an agent to confirm an access request. +var Confirm = binding.Bind[*ConfirmArguments, *ConfirmOK](command.MustParse("/access/confirm")) + +const ( + InvalidAccessConfirmSubjectErrorName = "InvalidAccessConfirmSubject" + InvalidAccessConfirmIssuerErrorName = "InvalidAccessConfirmIssuer" +) + +var ( + ErrInvalidAccessConfirmSubject = errors.New(InvalidAccessConfirmSubjectErrorName, "the subject of an access confirm invocation must be the service itself") + ErrInvalidAccessConfirmIssuer = errors.New(InvalidAccessConfirmIssuerErrorName, "the issuer of an access confirm invocation must be a valid mailto DID") +) diff --git a/protocol/commands/access/delegate.go b/protocol/commands/access/delegate.go new file mode 100644 index 00000000..17e48e62 --- /dev/null +++ b/protocol/commands/access/delegate.go @@ -0,0 +1,20 @@ +//go:build !codegen + +package access + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + command "github.com/fil-forge/ucantone/ucan/command" +) + +type DelegateOK = commands.Unit + +// Delegate can be invoked by an agent to delegate a set of capabilities that +// may be subsequently claimed by another agent. +var Delegate = binding.Bind[*DelegateArguments, *DelegateOK](command.MustParse("/access/delegate")) + +const ( + DelegationNotFoundErrorName = "DelegationNotFound" + InsufficientStorageErrorName = "InsufficientStorage" +) diff --git a/protocol/commands/access/gen/main.go b/protocol/commands/access/gen/main.go new file mode 100644 index 00000000..02025d88 --- /dev/null +++ b/protocol/commands/access/gen/main.go @@ -0,0 +1,47 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/access" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + access.RequestArguments{}, + access.CapabilityRequest{}, + access.RequestOK{}, + access.ClaimOK{}, + access.ConfirmArguments{}, + access.DelegateArguments{}, + access.GrantArguments{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "access", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "access", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/access/grant.go b/protocol/commands/access/grant.go new file mode 100644 index 00000000..74f984ad --- /dev/null +++ b/protocol/commands/access/grant.go @@ -0,0 +1,36 @@ +//go:build !codegen + +package access + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/ucan/command" +) + +const GrantCommand = "/access/grant" + +// GrantOK mirrors ClaimOK / ConfirmOK: a successful grant resolves into a +// bundle of delegation CIDs. The actual delegation envelopes ride in the +// receipt response container as metadata. +type GrantOK = ClaimOK + +// Grant can be invoked by an agent to request that a set of capabilities be +// granted directly. Unlike Request -> Confirm, Grant is one-shot: the +// executor decides immediately whether to issue the delegation. +var Grant = binding.Bind[*GrantArguments, *GrantOK](command.MustParse(GrantCommand)) + +const ( + UnknownAbilityErrorName = "UnknownAbility" + MissingCapabilityErrorName = "MissingCapability" + UnknownCauseErrorName = "UnknownCause" + MissingCauseErrorName = "MissingCause" + InvalidCauseErrorName = "InvalidCause" + UnauthorizedCauseErrorName = "UnauthorizedCause" +) + +var ( + ErrMissingCapability = errors.New(MissingCapabilityErrorName, "grant requires one or more capabilities") + ErrMissingCause = errors.New(MissingCauseErrorName, "grant requires a supporting contextual invocation") + ErrUnknownCause = errors.New(UnknownCauseErrorName, "unknown cause invocation") +) diff --git a/protocol/commands/access/json_gen.go b/protocol/commands/access/json_gen.go new file mode 100644 index 00000000..775be206 --- /dev/null +++ b/protocol/commands/access/json_gen.go @@ -0,0 +1,1163 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package access + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *RequestArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Attenuations ([]access.CapabilityRequest) (slice) + if len("att") > 8192 { + return fmt.Errorf("string in field \"att\" was too long") + } + if err := jw.WriteString(string("att")); err != nil { + return fmt.Errorf("writing string for field \"att\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Attenuations) > 8192 { + return fmt.Errorf("slice value in field t.Attenuations was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Attenuations: %w", err) + } + for i, v := range t.Attenuations { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Attenuations: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Attenuations: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Issuer (did.DID) (struct) + if len("iss") > 8192 { + return fmt.Errorf("string in field \"iss\" was too long") + } + if err := jw.WriteString(string("iss")); err != nil { + return fmt.Errorf("writing string for field \"iss\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Issuer.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Issuer: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RequestArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RequestArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RequestArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RequestArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RequestArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RequestArguments: string too large") + } + return fmt.Errorf("reading string for field RequestArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RequestArguments: %w", err) + } + switch name { + + // t.Attenuations ([]access.CapabilityRequest) (slice) + case "att": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Attenuations: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Attenuations: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Attenuations: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]CapabilityRequest, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Attenuations = append(t.Attenuations, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Attenuations: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Attenuations: slice too large") + } + } + } + + } + + // t.Issuer (did.DID) (struct) + case "iss": + + if err := t.Issuer.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Issuer: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RequestArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RequestArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RequestArguments") + } + } + } + + return nil +} +func (t *CapabilityRequest) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Command (command.Command) (struct) + if len("cmd") > 8192 { + return fmt.Errorf("string in field \"cmd\" was too long") + } + if err := jw.WriteString(string("cmd")); err != nil { + return fmt.Errorf("writing string for field \"cmd\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Command.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Command: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *CapabilityRequest) UnmarshalDagJSON(r io.Reader) (err error) { + *t = CapabilityRequest{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for CapabilityRequest: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for CapabilityRequest: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for CapabilityRequest: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field CapabilityRequest: string too large") + } + return fmt.Errorf("reading string for field CapabilityRequest: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field CapabilityRequest: %w", err) + } + switch name { + + // t.Command (command.Command) (struct) + case "cmd": + + if err := t.Command.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Command: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for CapabilityRequest: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field CapabilityRequest: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for CapabilityRequest") + } + } + } + + return nil +} +func (t *RequestOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Confirm (promise.AwaitOK) (struct) + if len("confirm") > 8192 { + return fmt.Errorf("string in field \"confirm\" was too long") + } + if err := jw.WriteString(string("confirm")); err != nil { + return fmt.Errorf("writing string for field \"confirm\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Confirm.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Confirm: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Expiration (int64) (int64) + if len("exp") > 8192 { + return fmt.Errorf("string in field \"exp\" was too long") + } + if err := jw.WriteString(string("exp")); err != nil { + return fmt.Errorf("writing string for field \"exp\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteInt64(int64(t.Expiration)); err != nil { + return fmt.Errorf("writing int64 for field t.Expiration: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Request (cid.Cid) (struct) + if len("req") > 8192 { + return fmt.Errorf("string in field \"req\" was too long") + } + if err := jw.WriteString(string("req")); err != nil { + return fmt.Errorf("writing string for field \"req\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Request); err != nil { + return fmt.Errorf("writing CID for field t.Request: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RequestOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RequestOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RequestOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RequestOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RequestOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RequestOK: string too large") + } + return fmt.Errorf("reading string for field RequestOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RequestOK: %w", err) + } + switch name { + + // t.Confirm (promise.AwaitOK) (struct) + case "confirm": + + if err := t.Confirm.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Confirm: %w", err) + } + + // t.Expiration (int64) (int64) + case "exp": + { + + nval, err := jr.ReadNumberAsInt64() + if err != nil { + return fmt.Errorf("reading int64 for field t.Expiration: %w", err) + } + t.Expiration = int64(nval) + + } + + // t.Request (cid.Cid) (struct) + case "req": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Request: %w", err) + } + t.Request = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RequestOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RequestOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RequestOK") + } + } + } + + return nil +} +func (t *ClaimOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Delegations ([]cid.Cid) (slice) + if len("delegations") > 8192 { + return fmt.Errorf("string in field \"delegations\" was too long") + } + if err := jw.WriteString(string("delegations")); err != nil { + return fmt.Errorf("writing string for field \"delegations\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Delegations) > 8192 { + return fmt.Errorf("slice value in field t.Delegations was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Delegations: %w", err) + } + for i, v := range t.Delegations { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Delegations: %w", err) + } + } + + if err := jw.WriteCid(v); err != nil { + return fmt.Errorf("writing CID for field v: %w", err) + } + + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Delegations: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ClaimOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ClaimOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ClaimOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ClaimOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ClaimOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ClaimOK: string too large") + } + return fmt.Errorf("reading string for field ClaimOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ClaimOK: %w", err) + } + switch name { + + // t.Delegations ([]cid.Cid) (slice) + case "delegations": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Delegations: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Delegations: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Delegations: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]cid.Cid, 1) + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field item[0]: %w", err) + } + item[0] = c + + } + t.Delegations = append(t.Delegations, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Delegations: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Delegations: slice too large") + } + } + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ClaimOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ClaimOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ClaimOK") + } + } + } + + return nil +} +func (t *ConfirmArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Attenuations ([]access.CapabilityRequest) (slice) + if len("att") > 8192 { + return fmt.Errorf("string in field \"att\" was too long") + } + if err := jw.WriteString(string("att")); err != nil { + return fmt.Errorf("writing string for field \"att\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Attenuations) > 8192 { + return fmt.Errorf("slice value in field t.Attenuations was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Attenuations: %w", err) + } + for i, v := range t.Attenuations { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Attenuations: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Attenuations: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Audience (did.DID) (struct) + if len("aud") > 8192 { + return fmt.Errorf("string in field \"aud\" was too long") + } + if err := jw.WriteString(string("aud")); err != nil { + return fmt.Errorf("writing string for field \"aud\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Audience.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Audience: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return fmt.Errorf("string in field \"cause\" was too long") + } + if err := jw.WriteString(string("cause")); err != nil { + return fmt.Errorf("writing string for field \"cause\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Cause); err != nil { + return fmt.Errorf("writing CID for field t.Cause: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Issuer (did.DID) (struct) + if len("iss") > 8192 { + return fmt.Errorf("string in field \"iss\" was too long") + } + if err := jw.WriteString(string("iss")); err != nil { + return fmt.Errorf("writing string for field \"iss\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Issuer.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Issuer: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ConfirmArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ConfirmArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ConfirmArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ConfirmArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ConfirmArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ConfirmArguments: string too large") + } + return fmt.Errorf("reading string for field ConfirmArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ConfirmArguments: %w", err) + } + switch name { + + // t.Attenuations ([]access.CapabilityRequest) (slice) + case "att": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Attenuations: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Attenuations: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Attenuations: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]CapabilityRequest, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Attenuations = append(t.Attenuations, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Attenuations: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Attenuations: slice too large") + } + } + } + + } + + // t.Audience (did.DID) (struct) + case "aud": + + if err := t.Audience.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Audience: %w", err) + } + + // t.Cause (cid.Cid) (struct) + case "cause": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Cause: %w", err) + } + t.Cause = c + + } + + // t.Issuer (did.DID) (struct) + case "iss": + + if err := t.Issuer.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Issuer: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ConfirmArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ConfirmArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ConfirmArguments") + } + } + } + + return nil +} +func (t *DelegateArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Delegations ([]cid.Cid) (slice) + if len("delegations") > 8192 { + return fmt.Errorf("string in field \"delegations\" was too long") + } + if err := jw.WriteString(string("delegations")); err != nil { + return fmt.Errorf("writing string for field \"delegations\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Delegations) > 8192 { + return fmt.Errorf("slice value in field t.Delegations was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Delegations: %w", err) + } + for i, v := range t.Delegations { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Delegations: %w", err) + } + } + + if err := jw.WriteCid(v); err != nil { + return fmt.Errorf("writing CID for field v: %w", err) + } + + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Delegations: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *DelegateArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = DelegateArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for DelegateArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for DelegateArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for DelegateArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field DelegateArguments: string too large") + } + return fmt.Errorf("reading string for field DelegateArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field DelegateArguments: %w", err) + } + switch name { + + // t.Delegations ([]cid.Cid) (slice) + case "delegations": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Delegations: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Delegations: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Delegations: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]cid.Cid, 1) + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field item[0]: %w", err) + } + item[0] = c + + } + t.Delegations = append(t.Delegations, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Delegations: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Delegations: slice too large") + } + } + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for DelegateArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field DelegateArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for DelegateArguments") + } + } + } + + return nil +} +func (t *GrantArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Attenuations ([]access.CapabilityRequest) (slice) + if len("att") > 8192 { + return fmt.Errorf("string in field \"att\" was too long") + } + if err := jw.WriteString(string("att")); err != nil { + return fmt.Errorf("writing string for field \"att\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Attenuations) > 8192 { + return fmt.Errorf("slice value in field t.Attenuations was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Attenuations: %w", err) + } + for i, v := range t.Attenuations { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Attenuations: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Attenuations: %w", err) + } + + written = true + if t.Cause != nil { + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + } + + // t.Cause (cid.Cid) (struct) + if t.Cause != nil { + if len("cause") > 8192 { + return fmt.Errorf("string in field \"cause\" was too long") + } + if err := jw.WriteString(string("cause")); err != nil { + return fmt.Errorf("writing string for field \"cause\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if t.Cause == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Cause: %w", err) + } + } else { + if err := jw.WriteCid(*t.Cause); err != nil { + return fmt.Errorf("writing CID for field t.Cause: %w", err) + } + } + + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *GrantArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = GrantArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for GrantArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for GrantArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for GrantArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field GrantArguments: string too large") + } + return fmt.Errorf("reading string for field GrantArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field GrantArguments: %w", err) + } + switch name { + + // t.Attenuations ([]access.CapabilityRequest) (slice) + case "att": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Attenuations: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Attenuations: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Attenuations: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]CapabilityRequest, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Attenuations = append(t.Attenuations, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Attenuations: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Attenuations: slice too large") + } + } + } + + } + + // t.Cause (cid.Cid) (struct) + case "cause": + { + + c, err := jr.ReadCidOrNull() + if err != nil { + return fmt.Errorf("reading CID or null for field t.Cause: %w", err) + } + t.Cause = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for GrantArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field GrantArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for GrantArguments") + } + } + } + + return nil +} diff --git a/protocol/commands/access/request.go b/protocol/commands/access/request.go new file mode 100644 index 00000000..10da7c06 --- /dev/null +++ b/protocol/commands/access/request.go @@ -0,0 +1,22 @@ +//go:build !codegen + +package access + +import ( + "github.com/fil-forge/ucantone/binding" + command "github.com/fil-forge/ucantone/ucan/command" +) + +// RequestFactKey is the key in metadata in any delegation created by a +// successful access request. The value is a link back to the `/access/request` +// invocation. +const RequestMetaKey = "accessRequest" + +// Request can be invoked by an agent to request set of capabilities from the +// account. +var Request = binding.Bind[*RequestArguments, *RequestOK](command.MustParse("/access/request")) + +const ( + InvalidAuthorizationAccountErrorName = "InvalidAuthorizationAccount" + InvalidAuthorizationAudienceErrorName = "InvalidAuthorizationAudience" +) diff --git a/protocol/commands/access/types.go b/protocol/commands/access/types.go new file mode 100644 index 00000000..a7e78f3f --- /dev/null +++ b/protocol/commands/access/types.go @@ -0,0 +1,54 @@ +package access + +import ( + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/promise" + "github.com/ipfs/go-cid" +) + +type ClaimOK struct { + // Delegations is a list of the CIDs of delegations granted for the request. + Delegations []cid.Cid `cborgen:"delegations" dagjsongen:"delegations"` +} + +type RequestArguments struct { + // DID of the Account authorization is requested from. + Issuer did.DID `cborgen:"iss" dagjsongen:"iss"` + // Capabilities agent wishes to be granted. + Attenuations []CapabilityRequest `cborgen:"att" dagjsongen:"att"` +} + +type CapabilityRequest struct { + Command ucan.Command `cborgen:"cmd" dagjsongen:"cmd"` +} + +type RequestOK struct { + // Request is a link to the access request invocation. + Request cid.Cid `cborgen:"req" dagjsongen:"req"` + // Confirm is the task that will confirm the access request. + Confirm promise.AwaitOK `cborgen:"confirm" dagjsongen:"confirm"` + // Expiration is the time at which the confirmation will expire. + Expiration int64 `cborgen:"exp" dagjsongen:"exp"` +} + +type ConfirmArguments struct { + Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` + Issuer did.DID `cborgen:"iss" dagjsongen:"iss"` + Audience did.DID `cborgen:"aud" dagjsongen:"aud"` + Attenuations []CapabilityRequest `cborgen:"att" dagjsongen:"att"` +} + +type DelegateArguments struct { + // The delegations to store. + Delegations []cid.Cid `cborgen:"delegations" dagjsongen:"delegations"` +} + +type GrantArguments struct { + // Attenuations are the capabilities the agent wishes to be granted. + Attenuations []CapabilityRequest `cborgen:"att" dagjsongen:"att"` + // Cause optionally links to a UCAN invocation that contextualizes the + // grant request. Leave nil when no cause is needed; otherwise the + // linked invocation MUST be present in the request container. + Cause *cid.Cid `cborgen:"cause,omitempty" dagjsongen:"cause,omitempty"` +} diff --git a/protocol/commands/assert/cbor_gen.go b/protocol/commands/assert/cbor_gen.go new file mode 100644 index 00000000..a4f7ff23 --- /dev/null +++ b/protocol/commands/assert/cbor_gen.go @@ -0,0 +1,798 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package assert + +import ( + "fmt" + "io" + "math" + "sort" + + commands "github.com/fil-forge/forge/protocol/commands" + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *IndexArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Index (cid.Cid) (struct) + if len("index") > 8192 { + return xerrors.Errorf("Value in field \"index\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("index"))); err != nil { + return err + } + if _, err := cw.WriteString(string("index")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Index); err != nil { + return xerrors.Errorf("failed to write cid field t.Index: %w", err) + } + + return nil +} + +func (t *IndexArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = IndexArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("IndexArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Index (cid.Cid) (struct) + case "index": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Index: %w", err) + } + + t.Index = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *IndexMetadata) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.RetrievalAuth ([]cid.Cid) (slice) + if len("retrievalAuth") > 8192 { + return xerrors.Errorf("Value in field \"retrievalAuth\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("retrievalAuth"))); err != nil { + return err + } + if _, err := cw.WriteString(string("retrievalAuth")); err != nil { + return err + } + + if len(t.RetrievalAuth) > 8192 { + return xerrors.Errorf("Slice value in field t.RetrievalAuth was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.RetrievalAuth))); err != nil { + return err + } + for _, v := range t.RetrievalAuth { + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + return nil +} + +func (t *IndexMetadata) UnmarshalCBOR(r io.Reader) (err error) { + *t = IndexMetadata{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("IndexMetadata: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 13) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.RetrievalAuth ([]cid.Cid) (slice) + case "retrievalAuth": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.RetrievalAuth: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.RetrievalAuth = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.RetrievalAuth[i]: %w", err) + } + + t.RetrievalAuth[i] = c + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *LocationArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 4 + + if t.Range == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Range (assert.Range) (struct) + if t.Range != nil { + + if len("range") > 8192 { + return xerrors.Errorf("Value in field \"range\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("range"))); err != nil { + return err + } + if _, err := cw.WriteString(string("range")); err != nil { + return err + } + + if err := t.Range.MarshalCBOR(cw); err != nil { + return err + } + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return xerrors.Errorf("Value in field \"space\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { + return err + } + if _, err := cw.WriteString(string("space")); err != nil { + return err + } + + if err := t.Space.MarshalCBOR(cw); err != nil { + return err + } + + // t.Content (multihash.Multihash) (slice) + if len("content") > 8192 { + return xerrors.Errorf("Value in field \"content\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("content"))); err != nil { + return err + } + if _, err := cw.WriteString(string("content")); err != nil { + return err + } + + if len(t.Content) > 2097152 { + return xerrors.Errorf("Byte array in field t.Content was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Content))); err != nil { + return err + } + + if _, err := cw.Write(t.Content); err != nil { + return err + } + + // t.Location ([]commands.CborURL) (slice) + if len("location") > 8192 { + return xerrors.Errorf("Value in field \"location\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("location"))); err != nil { + return err + } + if _, err := cw.WriteString(string("location")); err != nil { + return err + } + + if len(t.Location) > 8192 { + return xerrors.Errorf("Slice value in field t.Location was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Location))); err != nil { + return err + } + for _, v := range t.Location { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *LocationArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = LocationArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("LocationArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 8) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Range (assert.Range) (struct) + case "range": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Range = new(Range) + if err := t.Range.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Range pointer: %w", err) + } + } + + } + // t.Space (did.DID) (struct) + case "space": + + { + + if err := t.Space.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Space: %w", err) + } + + } + // t.Content (multihash.Multihash) (slice) + case "content": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Content: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Content = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Content); err != nil { + return err + } + + // t.Location ([]commands.CborURL) (slice) + case "location": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Location: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Location = make([]commands.CborURL, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Location[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Location[i]: %w", err) + } + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *Range) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 2 + + if t.End == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.End (uint64) (uint64) + if t.End != nil { + + if len("end") > 8192 { + return xerrors.Errorf("Value in field \"end\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("end"))); err != nil { + return err + } + if _, err := cw.WriteString(string("end")); err != nil { + return err + } + + if t.End == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.End)); err != nil { + return err + } + } + + } + + // t.Start (uint64) (uint64) + if len("start") > 8192 { + return xerrors.Errorf("Value in field \"start\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("start"))); err != nil { + return err + } + if _, err := cw.WriteString(string("start")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Start)); err != nil { + return err + } + + return nil +} + +func (t *Range) UnmarshalCBOR(r io.Reader) (err error) { + *t = Range{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Range: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.End (uint64) (uint64) + case "end": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + typed := uint64(extra) + t.End = &typed + } + + } + // t.Start (uint64) (uint64) + case "start": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Start = uint64(extra) + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *EqualsArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Equals (cid.Cid) (struct) + if len("equals") > 8192 { + return xerrors.Errorf("Value in field \"equals\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("equals"))); err != nil { + return err + } + if _, err := cw.WriteString(string("equals")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Equals); err != nil { + return xerrors.Errorf("failed to write cid field t.Equals: %w", err) + } + + // t.Content (multihash.Multihash) (slice) + if len("content") > 8192 { + return xerrors.Errorf("Value in field \"content\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("content"))); err != nil { + return err + } + if _, err := cw.WriteString(string("content")); err != nil { + return err + } + + if len(t.Content) > 2097152 { + return xerrors.Errorf("Byte array in field t.Content was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Content))); err != nil { + return err + } + + if _, err := cw.Write(t.Content); err != nil { + return err + } + + return nil +} + +func (t *EqualsArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = EqualsArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("EqualsArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Equals (cid.Cid) (struct) + case "equals": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Equals: %w", err) + } + + t.Equals = c + + } + // t.Content (multihash.Multihash) (slice) + case "content": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Content: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Content = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Content); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/assert/equals.go b/protocol/commands/assert/equals.go new file mode 100644 index 00000000..3cad6736 --- /dev/null +++ b/protocol/commands/assert/equals.go @@ -0,0 +1,14 @@ +//go:build !codegen + +package assert + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type EqualsOK = commands.Unit + +// Equals claims data is referred to by another CID e.g CAR CID & Piece CID +var Equals = binding.Bind[*EqualsArguments, *EqualsOK](command.MustParse("/assert/equals")) diff --git a/protocol/commands/assert/gen/main.go b/protocol/commands/assert/gen/main.go new file mode 100644 index 00000000..72e65c73 --- /dev/null +++ b/protocol/commands/assert/gen/main.go @@ -0,0 +1,45 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/assert" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + assert.IndexArguments{}, + assert.IndexMetadata{}, + assert.LocationArguments{}, + assert.Range{}, + assert.EqualsArguments{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "assert", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "assert", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/assert/index.go b/protocol/commands/assert/index.go new file mode 100644 index 00000000..09f4b41a --- /dev/null +++ b/protocol/commands/assert/index.go @@ -0,0 +1,15 @@ +//go:build !codegen + +package assert + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type IndexOK = commands.Unit + +// Index claims that a content graph can be found in blob(s) that are identified +// and indexed in the given index CID. +var Index = binding.Bind[*IndexArguments, *IndexOK](command.MustParse("/assert/index")) diff --git a/protocol/commands/assert/json_gen.go b/protocol/commands/assert/json_gen.go new file mode 100644 index 00000000..61aa3ff1 --- /dev/null +++ b/protocol/commands/assert/json_gen.go @@ -0,0 +1,808 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package assert + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + commands "github.com/fil-forge/forge/protocol/commands" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *IndexArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Index (cid.Cid) (struct) + if len("index") > 8192 { + return fmt.Errorf("string in field \"index\" was too long") + } + if err := jw.WriteString(string("index")); err != nil { + return fmt.Errorf("writing string for field \"index\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Index); err != nil { + return fmt.Errorf("writing CID for field t.Index: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *IndexArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = IndexArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for IndexArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for IndexArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for IndexArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field IndexArguments: string too large") + } + return fmt.Errorf("reading string for field IndexArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field IndexArguments: %w", err) + } + switch name { + + // t.Index (cid.Cid) (struct) + case "index": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Index: %w", err) + } + t.Index = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for IndexArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field IndexArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for IndexArguments") + } + } + } + + return nil +} +func (t *IndexMetadata) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.RetrievalAuth ([]cid.Cid) (slice) + if len("retrievalAuth") > 8192 { + return fmt.Errorf("string in field \"retrievalAuth\" was too long") + } + if err := jw.WriteString(string("retrievalAuth")); err != nil { + return fmt.Errorf("writing string for field \"retrievalAuth\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.RetrievalAuth) > 8192 { + return fmt.Errorf("slice value in field t.RetrievalAuth was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.RetrievalAuth: %w", err) + } + for i, v := range t.RetrievalAuth { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.RetrievalAuth: %w", err) + } + } + + if err := jw.WriteCid(v); err != nil { + return fmt.Errorf("writing CID for field v: %w", err) + } + + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.RetrievalAuth: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *IndexMetadata) UnmarshalDagJSON(r io.Reader) (err error) { + *t = IndexMetadata{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for IndexMetadata: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for IndexMetadata: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for IndexMetadata: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field IndexMetadata: string too large") + } + return fmt.Errorf("reading string for field IndexMetadata: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field IndexMetadata: %w", err) + } + switch name { + + // t.RetrievalAuth ([]cid.Cid) (slice) + case "retrievalAuth": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.RetrievalAuth: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.RetrievalAuth: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.RetrievalAuth: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]cid.Cid, 1) + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field item[0]: %w", err) + } + item[0] = c + + } + t.RetrievalAuth = append(t.RetrievalAuth, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.RetrievalAuth: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.RetrievalAuth: slice too large") + } + } + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for IndexMetadata: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field IndexMetadata: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for IndexMetadata") + } + } + } + + return nil +} +func (t *LocationArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Content (multihash.Multihash) (slice) + if len("content") > 8192 { + return fmt.Errorf("string in field \"content\" was too long") + } + if err := jw.WriteString(string("content")); err != nil { + return fmt.Errorf("writing string for field \"content\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Content) > 2097152 { + return fmt.Errorf("byte array in field t.Content was too long") + } + + if err := jw.WriteBytes(t.Content); err != nil { + return fmt.Errorf("writing bytes for field t.Content: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Location ([]commands.CborURL) (slice) + if len("location") > 8192 { + return fmt.Errorf("string in field \"location\" was too long") + } + if err := jw.WriteString(string("location")); err != nil { + return fmt.Errorf("writing string for field \"location\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Location) > 8192 { + return fmt.Errorf("slice value in field t.Location was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Location: %w", err) + } + for i, v := range t.Location { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Location: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Location: %w", err) + } + + written = true + if t.Range != nil { + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + } + + // t.Range (assert.Range) (struct) + if t.Range != nil { + if len("range") > 8192 { + return fmt.Errorf("string in field \"range\" was too long") + } + if err := jw.WriteString(string("range")); err != nil { + return fmt.Errorf("writing string for field \"range\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Range.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Range: %w", err) + } + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return fmt.Errorf("string in field \"space\" was too long") + } + if err := jw.WriteString(string("space")); err != nil { + return fmt.Errorf("writing string for field \"space\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Space.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Space: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *LocationArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = LocationArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for LocationArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for LocationArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for LocationArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field LocationArguments: string too large") + } + return fmt.Errorf("reading string for field LocationArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field LocationArguments: %w", err) + } + switch name { + + // t.Content (multihash.Multihash) (slice) + case "content": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Content: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Content: %w", err) + } + if len(bval) > 0 { + t.Content = []uint8(bval) + } + } + + // t.Location ([]commands.CborURL) (slice) + case "location": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Location: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Location: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Location: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]commands.CborURL, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Location = append(t.Location, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Location: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Location: slice too large") + } + } + } + + } + + // t.Range (assert.Range) (struct) + case "range": + + { + null, err := jr.PeekNull() + if err != nil { + return fmt.Errorf("peeking null for field t.Range: %w", err) + } + if null { + if err := jr.ReadNull(); err != nil { + return fmt.Errorf("reading null for field t.Range: %w", err) + } + } else { + t.Range = new(Range) + if err := t.Range.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Range pointer: %w", err) + } + } + } + + // t.Space (did.DID) (struct) + case "space": + + if err := t.Space.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Space: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for LocationArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field LocationArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for LocationArguments") + } + } + } + + return nil +} +func (t *Range) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.End (uint64) (uint64) + if t.End != nil { + if len("end") > 8192 { + return fmt.Errorf("string in field \"end\" was too long") + } + if err := jw.WriteString(string("end")); err != nil { + return fmt.Errorf("writing string for field \"end\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if t.End == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.End: %w", err) + } + } else { + if err := jw.WriteUint64(uint64(*t.End)); err != nil { + return fmt.Errorf("writing uint64 for field t.End: %w", err) + } + } + + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Start (uint64) (uint64) + if len("start") > 8192 { + return fmt.Errorf("string in field \"start\" was too long") + } + if err := jw.WriteString(string("start")); err != nil { + return fmt.Errorf("writing string for field \"start\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.Start)); err != nil { + return fmt.Errorf("writing uint64 for field t.Start: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *Range) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Range{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for Range: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for Range: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for Range: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field Range: string too large") + } + return fmt.Errorf("reading string for field Range: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field Range: %w", err) + } + switch name { + + // t.End (uint64) (uint64) + case "end": + { + + nval, err := jr.ReadNumberAsUint64OrNull() + if err != nil { + return fmt.Errorf("reading uint64 or null for field t.End: %w", err) + } + if nval != nil { + typed := uint64(*nval) + t.End = &typed + } + + } + + // t.Start (uint64) (uint64) + case "start": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Start: %w", err) + } + t.Start = uint64(nval) + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for Range: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field Range: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for Range") + } + } + } + + return nil +} +func (t *EqualsArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Content (multihash.Multihash) (slice) + if len("content") > 8192 { + return fmt.Errorf("string in field \"content\" was too long") + } + if err := jw.WriteString(string("content")); err != nil { + return fmt.Errorf("writing string for field \"content\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Content) > 2097152 { + return fmt.Errorf("byte array in field t.Content was too long") + } + + if err := jw.WriteBytes(t.Content); err != nil { + return fmt.Errorf("writing bytes for field t.Content: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Equals (cid.Cid) (struct) + if len("equals") > 8192 { + return fmt.Errorf("string in field \"equals\" was too long") + } + if err := jw.WriteString(string("equals")); err != nil { + return fmt.Errorf("writing string for field \"equals\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Equals); err != nil { + return fmt.Errorf("writing CID for field t.Equals: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *EqualsArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = EqualsArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for EqualsArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for EqualsArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for EqualsArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field EqualsArguments: string too large") + } + return fmt.Errorf("reading string for field EqualsArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field EqualsArguments: %w", err) + } + switch name { + + // t.Content (multihash.Multihash) (slice) + case "content": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Content: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Content: %w", err) + } + if len(bval) > 0 { + t.Content = []uint8(bval) + } + } + + // t.Equals (cid.Cid) (struct) + case "equals": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Equals: %w", err) + } + t.Equals = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for EqualsArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field EqualsArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for EqualsArguments") + } + } + } + + return nil +} diff --git a/protocol/commands/assert/location.go b/protocol/commands/assert/location.go new file mode 100644 index 00000000..55268742 --- /dev/null +++ b/protocol/commands/assert/location.go @@ -0,0 +1,13 @@ +//go:build !codegen + +package assert + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type LocationOK = commands.Unit + +var Location = binding.Bind[*LocationArguments, *LocationOK](command.MustParse("/assert/location")) diff --git a/protocol/commands/assert/types.go b/protocol/commands/assert/types.go new file mode 100644 index 00000000..b555277c --- /dev/null +++ b/protocol/commands/assert/types.go @@ -0,0 +1,36 @@ +package assert + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/did" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" +) + +type IndexArguments struct { + Index cid.Cid `cborgen:"index" dagjsongen:"index"` +} + +type IndexMetadata struct { + // RetrievalAuth is a list of CIDs of delegations (in order required for + // invocation) that can be used as proofs to issue a `/content/retrieve` + // invocation to retrieve an index blob. + RetrievalAuth []cid.Cid `cborgen:"retrievalAuth" dagjsongen:"retrievalAuth"` +} + +type LocationArguments struct { + Space did.DID `cborgen:"space" dagjsongen:"space"` + Content multihash.Multihash `cborgen:"content" dagjsongen:"content"` + Location []commands.CborURL `cborgen:"location" dagjsongen:"location"` + Range *Range `cborgen:"range,omitempty" dagjsongen:"range,omitempty"` +} + +type Range struct { + Start uint64 `cborgen:"start" dagjsongen:"start"` + End *uint64 `cborgen:"end,omitempty" dagjsongen:"end,omitempty"` +} + +type EqualsArguments struct { + Content multihash.Multihash `cborgen:"content" dagjsongen:"content"` + Equals cid.Cid `cborgen:"equals" dagjsongen:"equals"` +} diff --git a/protocol/commands/blob/abort.go b/protocol/commands/blob/abort.go new file mode 100644 index 00000000..38c5eb83 --- /dev/null +++ b/protocol/commands/blob/abort.go @@ -0,0 +1,40 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/ucan/command" +) + +type AbortOK = commands.Unit + +// Abort (/blob/abort) abandons an in-flight upload of a PARKED blob — +// allocated but never accepted, whether or not the bytes ever reached the +// storage node. It is the client-facing abandon verb: an upload ends in +// exactly one of `/blob/accept` (commit) or an abort that the upload service +// translates into `/blob/reject` on the storage node holding the allocation. +// +// Served by the upload service (subject = the space). A parked blob has no +// registration or acceptance to look the storage node up by, so the service +// recovers it from the Cause receipt chain and forwards a `/blob/reject` +// (Cause itself is not forwarded — it is routing metadata, meaningless to +// the node). A missing or unknown Cause fails with MissingCause. Blobs the +// space has accepted are released via `/blob/remove` instead; if the node +// refuses the translated reject with BlobAccepted, the service surfaces that +// named failure in the abort receipt. The abort mutates no upload-service +// state, so a failed abort is safely retryable. +// +// Idempotent: aborting an unknown or already-rejected blob succeeds. +// The receipt carries no payload (Unit). +var Abort = binding.Bind[*AbortArguments, *AbortOK](command.MustParse("/blob/abort")) + +// MissingCauseErrorName is the stable receipt-failure name when an abort's +// Cause is missing or does not resolve to a known `/blob/add` task — +// without it the upload service cannot recover which storage node holds the +// parked blob. +const MissingCauseErrorName = "MissingCause" + +var ErrMissingCause = errors.New(MissingCauseErrorName, "abort requires the cause of the /blob/add task that parked the blob") diff --git a/protocol/commands/blob/abort_test.go b/protocol/commands/blob/abort_test.go new file mode 100644 index 00000000..b1013879 --- /dev/null +++ b/protocol/commands/blob/abort_test.go @@ -0,0 +1,56 @@ +//go:build !codegen + +package blob_test + +import ( + "bytes" + "testing" + + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/ucantone/testutil" + "github.com/stretchr/testify/require" +) + +// Round-trips AbortArguments through cbor. +func TestAbortArgumentsRoundTrip(t *testing.T) { + in := blob.AbortArguments{ + Digest: testutil.RandomDigest(t), + Cause: testutil.RandomCID(t), + } + var buf bytes.Buffer + require.NoError(t, in.MarshalCBOR(&buf)) + var out blob.AbortArguments + require.NoError(t, out.UnmarshalCBOR(&buf)) + require.Equal(t, in.Digest, out.Digest) + require.Equal(t, in.Cause, out.Cause) +} + +// Round-trips ReleaseArguments through cbor. +func TestReleaseArgumentsRoundTrip(t *testing.T) { + in := blob.ReleaseArguments{ + Space: testutil.RandomDID(t), + Digest: testutil.RandomDigest(t), + Cause: testutil.RandomCID(t), + } + var buf bytes.Buffer + require.NoError(t, in.MarshalCBOR(&buf)) + var out blob.ReleaseArguments + require.NoError(t, out.UnmarshalCBOR(&buf)) + require.Equal(t, in.Space, out.Space) + require.Equal(t, in.Digest, out.Digest) + require.Equal(t, in.Cause, out.Cause) +} + +// Round-trips RejectArguments through cbor. +func TestRejectArgumentsRoundTrip(t *testing.T) { + in := blob.RejectArguments{ + Space: testutil.RandomDID(t), + Digest: testutil.RandomDigest(t), + } + var buf bytes.Buffer + require.NoError(t, in.MarshalCBOR(&buf)) + var out blob.RejectArguments + require.NoError(t, out.UnmarshalCBOR(&buf)) + require.Equal(t, in.Space, out.Space) + require.Equal(t, in.Digest, out.Digest) +} diff --git a/protocol/commands/blob/accept.go b/protocol/commands/blob/accept.go new file mode 100644 index 00000000..9494302f --- /dev/null +++ b/protocol/commands/blob/accept.go @@ -0,0 +1,10 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +var Accept = binding.Bind[*AcceptArguments, *AcceptOK](command.MustParse("/blob/accept")) diff --git a/protocol/commands/blob/add.go b/protocol/commands/blob/add.go new file mode 100644 index 00000000..8c2a918a --- /dev/null +++ b/protocol/commands/blob/add.go @@ -0,0 +1,10 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +var Add = binding.Bind[*AddArguments, *AddOK](command.MustParse("/blob/add")) diff --git a/protocol/commands/blob/allocate.go b/protocol/commands/blob/allocate.go new file mode 100644 index 00000000..5ca89b95 --- /dev/null +++ b/protocol/commands/blob/allocate.go @@ -0,0 +1,22 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// MaxBlobSize is the network-wide ceiling on a single blob, in bytes, as +// shipped to a storage node: the most raw bytes a default-configured piri +// accepts in one piece. Piri's default piece ceiling is a 256 MiB PADDED +// piece (piri pkg/pdp/piecesize, DefaultMaxPaddedSize), and Fr32 padding +// leaves 127/128 of it for data — 266338304 (~254 MiB). Piri's piece config +// is raise-only (an operator may accept more, never less), so a client +// sharding uploads at this constant is safe against every conforming node. +// +// This measures the stored artifact. An encrypting client must choose its +// plaintext split below this, leaving room for its envelope framing. +const MaxBlobSize = (256 << 20) * 127 / 128 + +var Allocate = binding.Bind[*AllocateArguments, *AllocateOK](command.MustParse("/blob/allocate")) diff --git a/protocol/commands/blob/cbor_gen.go b/protocol/commands/blob/cbor_gen.go new file mode 100644 index 00000000..b8701819 --- /dev/null +++ b/protocol/commands/blob/cbor_gen.go @@ -0,0 +1,2799 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package blob + +import ( + "fmt" + "io" + "math" + "sort" + + promise "github.com/fil-forge/ucantone/ucan/promise" + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *AllocateArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return xerrors.Errorf("Value in field \"cause\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cause"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cause")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Cause); err != nil { + return xerrors.Errorf("failed to write cid field t.Cause: %w", err) + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return xerrors.Errorf("Value in field \"space\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { + return err + } + if _, err := cw.WriteString(string("space")); err != nil { + return err + } + + if err := t.Space.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AllocateArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AllocateArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AllocateArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (blob.Blob) (struct) + case "blob": + + { + + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob: %w", err) + } + + } + // t.Cause (cid.Cid) (struct) + case "cause": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Cause: %w", err) + } + + t.Cause = c + + } + // t.Space (did.DID) (struct) + case "space": + + { + + if err := t.Space.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Space: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *Blob) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + return nil +} + +func (t *Blob) UnmarshalCBOR(r io.Reader) (err error) { + *t = Blob{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Blob: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Size (uint64) (uint64) + case "size": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Size = uint64(extra) + + } + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AllocateOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 2 + + if t.Address == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil { + return err + } + + // t.Address (blob.BlobAddress) (struct) + if t.Address != nil { + + if len("address") > 8192 { + return xerrors.Errorf("Value in field \"address\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("address"))); err != nil { + return err + } + if _, err := cw.WriteString(string("address")); err != nil { + return err + } + + if err := t.Address.MarshalCBOR(cw); err != nil { + return err + } + } + return nil +} + +func (t *AllocateOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = AllocateOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AllocateOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Size (uint64) (uint64) + case "size": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Size = uint64(extra) + + } + // t.Address (blob.BlobAddress) (struct) + case "address": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Address = new(BlobAddress) + if err := t.Address.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Address pointer: %w", err) + } + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *BlobAddress) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.URL (commands.CborURL) (struct) + if len("url") > 8192 { + return xerrors.Errorf("Value in field \"url\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("url"))); err != nil { + return err + } + if _, err := cw.WriteString(string("url")); err != nil { + return err + } + + if err := t.URL.MarshalCBOR(cw); err != nil { + return err + } + + // t.Expires (int64) (int64) + if len("expires") > 8192 { + return xerrors.Errorf("Value in field \"expires\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("expires"))); err != nil { + return err + } + if _, err := cw.WriteString(string("expires")); err != nil { + return err + } + + if t.Expires >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Expires)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Expires-1)); err != nil { + return err + } + } + + // t.Headers (map[string]string) (map) + if len("headers") > 8192 { + return xerrors.Errorf("Value in field \"headers\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("headers"))); err != nil { + return err + } + if _, err := cw.WriteString(string("headers")); err != nil { + return err + } + + { + if len(t.Headers) > 4096 { + return xerrors.Errorf("cannot marshal t.Headers map too large") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajMap, uint64(len(t.Headers))); err != nil { + return err + } + + keys := make([]string, 0, len(t.Headers)) + for k := range t.Headers { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := t.Headers[k] + + if len(k) > 8192 { + return xerrors.Errorf("Value in field k was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(k))); err != nil { + return err + } + if _, err := cw.WriteString(string(k)); err != nil { + return err + } + + if len(v) > 8192 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + return nil +} + +func (t *BlobAddress) UnmarshalCBOR(r io.Reader) (err error) { + *t = BlobAddress{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("BlobAddress: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.URL (commands.CborURL) (struct) + case "url": + + { + + if err := t.URL.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.URL: %w", err) + } + + } + // t.Expires (int64) (int64) + case "expires": + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.Expires = int64(extraI) + } + // t.Headers (map[string]string) (map) + case "headers": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajMap { + return fmt.Errorf("expected a map (major type 5)") + } + if extra > 4096 { + return fmt.Errorf("t.Headers: map too large") + } + + t.Headers = make(map[string]string, extra) + + for i, l := 0, int(extra); i < l; i++ { + + var k string + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + k = string(sval) + } + + var v string + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + v = string(sval) + } + + t.Headers[k] = v + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AcceptArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.Put (promise.AwaitOK) (struct) + if len("_put") > 8192 { + return xerrors.Errorf("Value in field \"_put\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("_put"))); err != nil { + return err + } + if _, err := cw.WriteString(string("_put")); err != nil { + return err + } + + if err := t.Put.MarshalCBOR(cw); err != nil { + return err + } + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return xerrors.Errorf("Value in field \"space\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { + return err + } + if _, err := cw.WriteString(string("space")); err != nil { + return err + } + + if err := t.Space.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AcceptArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AcceptArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AcceptArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Put (promise.AwaitOK) (struct) + case "_put": + + { + + if err := t.Put.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Put: %w", err) + } + + } + // t.Blob (blob.Blob) (struct) + case "blob": + + { + + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob: %w", err) + } + + } + // t.Space (did.DID) (struct) + case "space": + + { + + if err := t.Space.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Space: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AcceptOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.PDP (promise.AwaitOK) (struct) + if len("pdp") > 8192 { + return xerrors.Errorf("Value in field \"pdp\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pdp"))); err != nil { + return err + } + if _, err := cw.WriteString(string("pdp")); err != nil { + return err + } + + if err := t.PDP.MarshalCBOR(cw); err != nil { + return err + } + + // t.Site (cid.Cid) (struct) + if len("site") > 8192 { + return xerrors.Errorf("Value in field \"site\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("site"))); err != nil { + return err + } + if _, err := cw.WriteString(string("site")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Site); err != nil { + return xerrors.Errorf("failed to write cid field t.Site: %w", err) + } + + return nil +} + +func (t *AcceptOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = AcceptOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AcceptOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.PDP (promise.AwaitOK) (struct) + case "pdp": + + { + + if err := t.PDP.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.PDP: %w", err) + } + + } + // t.Site (cid.Cid) (struct) + case "site": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Site: %w", err) + } + + t.Site = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AddArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AddArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AddArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AddArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (blob.Blob) (struct) + case "blob": + + { + + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AddOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Site (promise.AwaitOK) (struct) + if len("site") > 8192 { + return xerrors.Errorf("Value in field \"site\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("site"))); err != nil { + return err + } + if _, err := cw.WriteString(string("site")); err != nil { + return err + } + + if err := t.Site.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AddOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = AddOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AddOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Site (promise.AwaitOK) (struct) + case "site": + + { + + if err := t.Site.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Site: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RemoveArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + return nil +} + +func (t *RemoveArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RemoveArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RemoveArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ReleaseArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return xerrors.Errorf("Value in field \"cause\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cause"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cause")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Cause); err != nil { + return xerrors.Errorf("failed to write cid field t.Cause: %w", err) + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return xerrors.Errorf("Value in field \"space\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { + return err + } + if _, err := cw.WriteString(string("space")); err != nil { + return err + } + + if err := t.Space.MarshalCBOR(cw); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + return nil +} + +func (t *ReleaseArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ReleaseArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ReleaseArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Cause (cid.Cid) (struct) + case "cause": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Cause: %w", err) + } + + t.Cause = c + + } + // t.Space (did.DID) (struct) + case "space": + + { + + if err := t.Space.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Space: %w", err) + } + + } + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AbortArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return xerrors.Errorf("Value in field \"cause\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cause"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cause")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Cause); err != nil { + return xerrors.Errorf("failed to write cid field t.Cause: %w", err) + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + return nil +} + +func (t *AbortArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AbortArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AbortArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Cause (cid.Cid) (struct) + case "cause": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Cause: %w", err) + } + + t.Cause = c + + } + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RejectArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return xerrors.Errorf("Value in field \"space\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { + return err + } + if _, err := cw.WriteString(string("space")); err != nil { + return err + } + + if err := t.Space.MarshalCBOR(cw); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + return nil +} + +func (t *RejectArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RejectArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RejectArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Space (did.DID) (struct) + case "space": + + { + + if err := t.Space.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Space: %w", err) + } + + } + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ReplicateArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + + // t.Site (cid.Cid) (struct) + if len("site") > 8192 { + return xerrors.Errorf("Value in field \"site\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("site"))); err != nil { + return err + } + if _, err := cw.WriteString(string("site")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Site); err != nil { + return xerrors.Errorf("failed to write cid field t.Site: %w", err) + } + + // t.Replicas (uint64) (uint64) + if len("replicas") > 8192 { + return xerrors.Errorf("Value in field \"replicas\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("replicas"))); err != nil { + return err + } + if _, err := cw.WriteString(string("replicas")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Replicas)); err != nil { + return err + } + + return nil +} + +func (t *ReplicateArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ReplicateArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ReplicateArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 8) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (blob.Blob) (struct) + case "blob": + + { + + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob: %w", err) + } + + } + // t.Site (cid.Cid) (struct) + case "site": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Site: %w", err) + } + + t.Site = c + + } + // t.Replicas (uint64) (uint64) + case "replicas": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Replicas = uint64(extra) + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ReplicateOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Site ([]promise.AwaitOK) (slice) + if len("site") > 8192 { + return xerrors.Errorf("Value in field \"site\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("site"))); err != nil { + return err + } + if _, err := cw.WriteString(string("site")); err != nil { + return err + } + + if len(t.Site) > 8192 { + return xerrors.Errorf("Slice value in field t.Site was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Site))); err != nil { + return err + } + for _, v := range t.Site { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *ReplicateOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = ReplicateOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ReplicateOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Site ([]promise.AwaitOK) (slice) + case "site": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Site: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Site = make([]promise.AwaitOK, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Site[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Site[i]: %w", err) + } + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ListArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 2 + + if t.Cursor == nil { + fieldCount-- + } + + if t.Size == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Size (uint64) (uint64) + if t.Size != nil { + + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if t.Size == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.Size)); err != nil { + return err + } + } + + } + + // t.Cursor (string) (string) + if t.Cursor != nil { + + if len("cursor") > 8192 { + return xerrors.Errorf("Value in field \"cursor\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cursor"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cursor")); err != nil { + return err + } + + if t.Cursor == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Cursor) > 8192 { + return xerrors.Errorf("Value in field t.Cursor was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Cursor))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Cursor)); err != nil { + return err + } + } + } + return nil +} + +func (t *ListArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Size (uint64) (uint64) + case "size": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + typed := uint64(extra) + t.Size = &typed + } + + } + // t.Cursor (string) (string) + case "cursor": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Cursor = (*string)(&sval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ListOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 3 + + if t.Cursor == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil { + return err + } + + // t.Cursor (string) (string) + if t.Cursor != nil { + + if len("cursor") > 8192 { + return xerrors.Errorf("Value in field \"cursor\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cursor"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cursor")); err != nil { + return err + } + + if t.Cursor == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Cursor) > 8192 { + return xerrors.Errorf("Value in field t.Cursor was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Cursor))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Cursor)); err != nil { + return err + } + } + } + + // t.Results ([]blob.ListBlobItem) (slice) + if len("results") > 8192 { + return xerrors.Errorf("Value in field \"results\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("results"))); err != nil { + return err + } + if _, err := cw.WriteString(string("results")); err != nil { + return err + } + + if len(t.Results) > 8192 { + return xerrors.Errorf("Slice value in field t.Results was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Results))); err != nil { + return err + } + for _, v := range t.Results { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *ListOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Size (uint64) (uint64) + case "size": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Size = uint64(extra) + + } + // t.Cursor (string) (string) + case "cursor": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Cursor = (*string)(&sval) + } + } + // t.Results ([]blob.ListBlobItem) (slice) + case "results": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Results: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Results = make([]ListBlobItem, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Results[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Results[i]: %w", err) + } + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ListBlobItem) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + + // t.InsertedAt (int64) (int64) + if len("insertedAt") > 8192 { + return xerrors.Errorf("Value in field \"insertedAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("insertedAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("insertedAt")); err != nil { + return err + } + + if t.InsertedAt >= 0 { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.InsertedAt)); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.InsertedAt-1)); err != nil { + return err + } + } + + return nil +} + +func (t *ListBlobItem) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListBlobItem{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListBlobItem: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (blob.Blob) (struct) + case "blob": + + { + + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob: %w", err) + } + + } + // t.InsertedAt (int64) (int64) + case "insertedAt": + { + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + var extraI int64 + switch maj { + case cbg.MajUnsignedInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 positive overflow") + } + case cbg.MajNegativeInt: + extraI = int64(extra) + if extraI < 0 { + return fmt.Errorf("int64 negative overflow") + } + extraI = -1 - extraI + default: + return fmt.Errorf("wrong type for int64 field: %d", maj) + } + + t.InsertedAt = int64(extraI) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RetrieveArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Blob (blob.RetrieveBlob) (struct) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *RetrieveArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RetrieveArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RetrieveArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (blob.RetrieveBlob) (struct) + case "blob": + + { + + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RetrieveBlob) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + return nil +} + +func (t *RetrieveBlob) UnmarshalCBOR(r io.Reader) (err error) { + *t = RetrieveBlob{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RetrieveBlob: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RetrieveOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{160}); err != nil { + return err + } + return nil +} + +func (t *RetrieveOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = RetrieveOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RetrieveOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 0) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/blob/gen/main.go b/protocol/commands/blob/gen/main.go new file mode 100644 index 00000000..85382500 --- /dev/null +++ b/protocol/commands/blob/gen/main.go @@ -0,0 +1,60 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/blob" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + blob.AllocateArguments{}, + blob.Blob{}, + blob.AllocateOK{}, + blob.BlobAddress{}, + blob.AcceptArguments{}, + blob.AcceptOK{}, + blob.AddArguments{}, + blob.AddOK{}, + blob.RemoveArguments{}, + blob.ReleaseArguments{}, + blob.AbortArguments{}, + blob.RejectArguments{}, + blob.ReplicateArguments{}, + blob.ReplicateOK{}, + blob.ListArguments{}, + blob.ListOK{}, + blob.ListBlobItem{}, + blob.RetrieveArguments{}, + blob.RetrieveBlob{}, + blob.RetrieveOK{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "blob", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "blob", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/blob/json_gen.go b/protocol/commands/blob/json_gen.go new file mode 100644 index 00000000..11d00108 --- /dev/null +++ b/protocol/commands/blob/json_gen.go @@ -0,0 +1,2766 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package blob + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + promise "github.com/fil-forge/ucantone/ucan/promise" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *AllocateArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return fmt.Errorf("string in field \"blob\" was too long") + } + if err := jw.WriteString(string("blob")); err != nil { + return fmt.Errorf("writing string for field \"blob\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Blob.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Blob: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return fmt.Errorf("string in field \"cause\" was too long") + } + if err := jw.WriteString(string("cause")); err != nil { + return fmt.Errorf("writing string for field \"cause\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Cause); err != nil { + return fmt.Errorf("writing CID for field t.Cause: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return fmt.Errorf("string in field \"space\" was too long") + } + if err := jw.WriteString(string("space")); err != nil { + return fmt.Errorf("writing string for field \"space\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Space.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Space: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AllocateArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AllocateArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AllocateArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AllocateArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AllocateArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AllocateArguments: string too large") + } + return fmt.Errorf("reading string for field AllocateArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AllocateArguments: %w", err) + } + switch name { + + // t.Blob (blob.Blob) (struct) + case "blob": + + if err := t.Blob.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Blob: %w", err) + } + + // t.Cause (cid.Cid) (struct) + case "cause": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Cause: %w", err) + } + t.Cause = c + + } + + // t.Space (did.DID) (struct) + case "space": + + if err := t.Space.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Space: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AllocateArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AllocateArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AllocateArguments") + } + } + } + + return nil +} +func (t *Blob) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *Blob) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Blob{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for Blob: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for Blob: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for Blob: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field Blob: string too large") + } + return fmt.Errorf("reading string for field Blob: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field Blob: %w", err) + } + switch name { + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Size: %w", err) + } + t.Size = uint64(nval) + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for Blob: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field Blob: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for Blob") + } + } + } + + return nil +} +func (t *AllocateOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Address (blob.BlobAddress) (struct) + if t.Address != nil { + if len("address") > 8192 { + return fmt.Errorf("string in field \"address\" was too long") + } + if err := jw.WriteString(string("address")); err != nil { + return fmt.Errorf("writing string for field \"address\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Address.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Address: %w", err) + } + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AllocateOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AllocateOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AllocateOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AllocateOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AllocateOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AllocateOK: string too large") + } + return fmt.Errorf("reading string for field AllocateOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AllocateOK: %w", err) + } + switch name { + + // t.Address (blob.BlobAddress) (struct) + case "address": + + { + null, err := jr.PeekNull() + if err != nil { + return fmt.Errorf("peeking null for field t.Address: %w", err) + } + if null { + if err := jr.ReadNull(); err != nil { + return fmt.Errorf("reading null for field t.Address: %w", err) + } + } else { + t.Address = new(BlobAddress) + if err := t.Address.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Address pointer: %w", err) + } + } + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Size: %w", err) + } + t.Size = uint64(nval) + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AllocateOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AllocateOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AllocateOK") + } + } + } + + return nil +} +func (t *BlobAddress) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Expires (int64) (int64) + if len("expires") > 8192 { + return fmt.Errorf("string in field \"expires\" was too long") + } + if err := jw.WriteString(string("expires")); err != nil { + return fmt.Errorf("writing string for field \"expires\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteInt64(int64(t.Expires)); err != nil { + return fmt.Errorf("writing int64 for field t.Expires: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Headers (map[string]string) (map) + if len("headers") > 8192 { + return fmt.Errorf("string in field \"headers\" was too long") + } + if err := jw.WriteString(string("headers")); err != nil { + return fmt.Errorf("writing string for field \"headers\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + { + if len(t.Headers) > 4096 { + return fmt.Errorf("cannot marshal t.Headers map too large") + } + + if err := jw.WriteObjectOpen(); err != nil { + return fmt.Errorf("writing object open for field t.Headers: %w", err) + } + + keys := make([]string, 0, len(t.Headers)) + for k := range t.Headers { + keys = append(keys, k) + } + sort.Strings(keys) + for i, k := range keys { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Headers: %w", err) + } + } + v := t.Headers[k] + if len(k) > 8192 { + return fmt.Errorf("string in field k was too long") + } + if err := jw.WriteString(string(k)); err != nil { + return fmt.Errorf("writing string for field k: %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return fmt.Errorf("writing colon for field t.Headers: %w", err) + } + + if len(v) > 8192 { + return fmt.Errorf("string in field v was too long") + } + if err := jw.WriteString(string(v)); err != nil { + return fmt.Errorf("writing string for field v: %w", err) + } + } + if err := jw.WriteObjectClose(); err != nil { + return fmt.Errorf("writing object close for field t.Headers: %w", err) + } + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.URL (commands.CborURL) (struct) + if len("url") > 8192 { + return fmt.Errorf("string in field \"url\" was too long") + } + if err := jw.WriteString(string("url")); err != nil { + return fmt.Errorf("writing string for field \"url\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.URL.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.URL: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *BlobAddress) UnmarshalDagJSON(r io.Reader) (err error) { + *t = BlobAddress{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for BlobAddress: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for BlobAddress: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for BlobAddress: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field BlobAddress: string too large") + } + return fmt.Errorf("reading string for field BlobAddress: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field BlobAddress: %w", err) + } + switch name { + + // t.Expires (int64) (int64) + case "expires": + { + + nval, err := jr.ReadNumberAsInt64() + if err != nil { + return fmt.Errorf("reading int64 for field t.Expires: %w", err) + } + t.Expires = int64(nval) + + } + + // t.Headers (map[string]string) (map) + case "headers": + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for field t.Headers: %w", err) + } + + t.Headers = map[string]string{} + + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for field t.Headers: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for field t.Headers: %w", err) + } + } else { + for i, l := 0, 8192; i < l; i++ { + var k string + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field k: string too long") + } + return fmt.Errorf("reading string for field k: %w", err) + } + k = string(sval) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field t.Headers: %w", err) + } + var v string + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field v: string too long") + } + return fmt.Errorf("reading string for field v: %w", err) + } + v = string(sval) + } + t.Headers[k] = v + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field t.Headers: %w", err) + } + if close { + break + } + } + } + + // t.URL (commands.CborURL) (struct) + case "url": + + if err := t.URL.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.URL: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for BlobAddress: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field BlobAddress: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for BlobAddress") + } + } + } + + return nil +} +func (t *AcceptArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Put (promise.AwaitOK) (struct) + if len("_put") > 8192 { + return fmt.Errorf("string in field \"_put\" was too long") + } + if err := jw.WriteString(string("_put")); err != nil { + return fmt.Errorf("writing string for field \"_put\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Put.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Put: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return fmt.Errorf("string in field \"blob\" was too long") + } + if err := jw.WriteString(string("blob")); err != nil { + return fmt.Errorf("writing string for field \"blob\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Blob.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Blob: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return fmt.Errorf("string in field \"space\" was too long") + } + if err := jw.WriteString(string("space")); err != nil { + return fmt.Errorf("writing string for field \"space\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Space.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Space: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AcceptArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AcceptArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AcceptArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AcceptArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AcceptArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AcceptArguments: string too large") + } + return fmt.Errorf("reading string for field AcceptArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AcceptArguments: %w", err) + } + switch name { + + // t.Put (promise.AwaitOK) (struct) + case "_put": + + if err := t.Put.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Put: %w", err) + } + + // t.Blob (blob.Blob) (struct) + case "blob": + + if err := t.Blob.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Blob: %w", err) + } + + // t.Space (did.DID) (struct) + case "space": + + if err := t.Space.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Space: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AcceptArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AcceptArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AcceptArguments") + } + } + } + + return nil +} +func (t *AcceptOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.PDP (promise.AwaitOK) (struct) + if len("pdp") > 8192 { + return fmt.Errorf("string in field \"pdp\" was too long") + } + if err := jw.WriteString(string("pdp")); err != nil { + return fmt.Errorf("writing string for field \"pdp\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.PDP.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.PDP: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Site (cid.Cid) (struct) + if len("site") > 8192 { + return fmt.Errorf("string in field \"site\" was too long") + } + if err := jw.WriteString(string("site")); err != nil { + return fmt.Errorf("writing string for field \"site\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Site); err != nil { + return fmt.Errorf("writing CID for field t.Site: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AcceptOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AcceptOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AcceptOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AcceptOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AcceptOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AcceptOK: string too large") + } + return fmt.Errorf("reading string for field AcceptOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AcceptOK: %w", err) + } + switch name { + + // t.PDP (promise.AwaitOK) (struct) + case "pdp": + + if err := t.PDP.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.PDP: %w", err) + } + + // t.Site (cid.Cid) (struct) + case "site": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Site: %w", err) + } + t.Site = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AcceptOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AcceptOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AcceptOK") + } + } + } + + return nil +} +func (t *AddArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return fmt.Errorf("string in field \"blob\" was too long") + } + if err := jw.WriteString(string("blob")); err != nil { + return fmt.Errorf("writing string for field \"blob\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Blob.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Blob: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AddArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AddArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AddArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AddArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AddArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AddArguments: string too large") + } + return fmt.Errorf("reading string for field AddArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AddArguments: %w", err) + } + switch name { + + // t.Blob (blob.Blob) (struct) + case "blob": + + if err := t.Blob.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Blob: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AddArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AddArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AddArguments") + } + } + } + + return nil +} +func (t *AddOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Site (promise.AwaitOK) (struct) + if len("site") > 8192 { + return fmt.Errorf("string in field \"site\" was too long") + } + if err := jw.WriteString(string("site")); err != nil { + return fmt.Errorf("writing string for field \"site\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Site.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Site: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AddOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AddOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AddOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AddOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AddOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AddOK: string too large") + } + return fmt.Errorf("reading string for field AddOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AddOK: %w", err) + } + switch name { + + // t.Site (promise.AwaitOK) (struct) + case "site": + + if err := t.Site.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Site: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AddOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AddOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AddOK") + } + } + } + + return nil +} +func (t *RemoveArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RemoveArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RemoveArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RemoveArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RemoveArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RemoveArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RemoveArguments: string too large") + } + return fmt.Errorf("reading string for field RemoveArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RemoveArguments: %w", err) + } + switch name { + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RemoveArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RemoveArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RemoveArguments") + } + } + } + + return nil +} +func (t *ReleaseArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return fmt.Errorf("string in field \"cause\" was too long") + } + if err := jw.WriteString(string("cause")); err != nil { + return fmt.Errorf("writing string for field \"cause\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Cause); err != nil { + return fmt.Errorf("writing CID for field t.Cause: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return fmt.Errorf("string in field \"space\" was too long") + } + if err := jw.WriteString(string("space")); err != nil { + return fmt.Errorf("writing string for field \"space\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Space.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Space: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ReleaseArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ReleaseArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ReleaseArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ReleaseArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ReleaseArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ReleaseArguments: string too large") + } + return fmt.Errorf("reading string for field ReleaseArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ReleaseArguments: %w", err) + } + switch name { + + // t.Cause (cid.Cid) (struct) + case "cause": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Cause: %w", err) + } + t.Cause = c + + } + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + // t.Space (did.DID) (struct) + case "space": + + if err := t.Space.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Space: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ReleaseArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ReleaseArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ReleaseArguments") + } + } + } + + return nil +} +func (t *AbortArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return fmt.Errorf("string in field \"cause\" was too long") + } + if err := jw.WriteString(string("cause")); err != nil { + return fmt.Errorf("writing string for field \"cause\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Cause); err != nil { + return fmt.Errorf("writing CID for field t.Cause: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AbortArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AbortArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AbortArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AbortArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AbortArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AbortArguments: string too large") + } + return fmt.Errorf("reading string for field AbortArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AbortArguments: %w", err) + } + switch name { + + // t.Cause (cid.Cid) (struct) + case "cause": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Cause: %w", err) + } + t.Cause = c + + } + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AbortArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AbortArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AbortArguments") + } + } + } + + return nil +} +func (t *RejectArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Space (did.DID) (struct) + if len("space") > 8192 { + return fmt.Errorf("string in field \"space\" was too long") + } + if err := jw.WriteString(string("space")); err != nil { + return fmt.Errorf("writing string for field \"space\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Space.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Space: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RejectArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RejectArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RejectArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RejectArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RejectArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RejectArguments: string too large") + } + return fmt.Errorf("reading string for field RejectArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RejectArguments: %w", err) + } + switch name { + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + // t.Space (did.DID) (struct) + case "space": + + if err := t.Space.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Space: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RejectArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RejectArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RejectArguments") + } + } + } + + return nil +} +func (t *ReplicateArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return fmt.Errorf("string in field \"blob\" was too long") + } + if err := jw.WriteString(string("blob")); err != nil { + return fmt.Errorf("writing string for field \"blob\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Blob.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Blob: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Replicas (uint64) (uint64) + if len("replicas") > 8192 { + return fmt.Errorf("string in field \"replicas\" was too long") + } + if err := jw.WriteString(string("replicas")); err != nil { + return fmt.Errorf("writing string for field \"replicas\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.Replicas)); err != nil { + return fmt.Errorf("writing uint64 for field t.Replicas: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Site (cid.Cid) (struct) + if len("site") > 8192 { + return fmt.Errorf("string in field \"site\" was too long") + } + if err := jw.WriteString(string("site")); err != nil { + return fmt.Errorf("writing string for field \"site\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Site); err != nil { + return fmt.Errorf("writing CID for field t.Site: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ReplicateArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ReplicateArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ReplicateArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ReplicateArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ReplicateArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ReplicateArguments: string too large") + } + return fmt.Errorf("reading string for field ReplicateArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ReplicateArguments: %w", err) + } + switch name { + + // t.Blob (blob.Blob) (struct) + case "blob": + + if err := t.Blob.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Blob: %w", err) + } + + // t.Replicas (uint64) (uint64) + case "replicas": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Replicas: %w", err) + } + t.Replicas = uint64(nval) + + } + + // t.Site (cid.Cid) (struct) + case "site": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Site: %w", err) + } + t.Site = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ReplicateArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ReplicateArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ReplicateArguments") + } + } + } + + return nil +} +func (t *ReplicateOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Site ([]promise.AwaitOK) (slice) + if len("site") > 8192 { + return fmt.Errorf("string in field \"site\" was too long") + } + if err := jw.WriteString(string("site")); err != nil { + return fmt.Errorf("writing string for field \"site\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Site) > 8192 { + return fmt.Errorf("slice value in field t.Site was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Site: %w", err) + } + for i, v := range t.Site { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Site: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Site: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ReplicateOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ReplicateOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ReplicateOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ReplicateOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ReplicateOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ReplicateOK: string too large") + } + return fmt.Errorf("reading string for field ReplicateOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ReplicateOK: %w", err) + } + switch name { + + // t.Site ([]promise.AwaitOK) (slice) + case "site": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Site: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Site: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Site: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]promise.AwaitOK, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Site = append(t.Site, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Site: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Site: slice too large") + } + } + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ReplicateOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ReplicateOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ReplicateOK") + } + } + } + + return nil +} +func (t *ListArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Cursor (string) (string) + if t.Cursor != nil { + if len("cursor") > 8192 { + return fmt.Errorf("string in field \"cursor\" was too long") + } + if err := jw.WriteString(string("cursor")); err != nil { + return fmt.Errorf("writing string for field \"cursor\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if t.Cursor == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Cursor: %w", err) + } + } else { + if len(*t.Cursor) > 8192 { + return fmt.Errorf("string in field t.Cursor was too long") + } + if err := jw.WriteString(string(*t.Cursor)); err != nil { + return fmt.Errorf("writing string for field t.Cursor: %w", err) + } + } + written = true + } + if t.Size != nil { + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + } + + // t.Size (uint64) (uint64) + if t.Size != nil { + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if t.Size == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Size: %w", err) + } + } else { + if err := jw.WriteUint64(uint64(*t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + } + + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListArguments: string too large") + } + return fmt.Errorf("reading string for field ListArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListArguments: %w", err) + } + switch name { + + // t.Cursor (string) (string) + case "cursor": + { + sval, err := jr.ReadStringOrNull(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string or null for field t.Cursor: string too long") + } + return fmt.Errorf("reading string or null for field t.Cursor: %w", err) + } + if sval != nil { + t.Cursor = (*string)(sval) + } + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64OrNull() + if err != nil { + return fmt.Errorf("reading uint64 or null for field t.Size: %w", err) + } + if nval != nil { + typed := uint64(*nval) + t.Size = &typed + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListArguments") + } + } + } + + return nil +} +func (t *ListOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Cursor (string) (string) + if t.Cursor != nil { + if len("cursor") > 8192 { + return fmt.Errorf("string in field \"cursor\" was too long") + } + if err := jw.WriteString(string("cursor")); err != nil { + return fmt.Errorf("writing string for field \"cursor\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if t.Cursor == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Cursor: %w", err) + } + } else { + if len(*t.Cursor) > 8192 { + return fmt.Errorf("string in field t.Cursor was too long") + } + if err := jw.WriteString(string(*t.Cursor)); err != nil { + return fmt.Errorf("writing string for field t.Cursor: %w", err) + } + } + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Results ([]blob.ListBlobItem) (slice) + if len("results") > 8192 { + return fmt.Errorf("string in field \"results\" was too long") + } + if err := jw.WriteString(string("results")); err != nil { + return fmt.Errorf("writing string for field \"results\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Results) > 8192 { + return fmt.Errorf("slice value in field t.Results was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Results: %w", err) + } + for i, v := range t.Results { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Results: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Results: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListOK: string too large") + } + return fmt.Errorf("reading string for field ListOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListOK: %w", err) + } + switch name { + + // t.Cursor (string) (string) + case "cursor": + { + sval, err := jr.ReadStringOrNull(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string or null for field t.Cursor: string too long") + } + return fmt.Errorf("reading string or null for field t.Cursor: %w", err) + } + if sval != nil { + t.Cursor = (*string)(sval) + } + } + + // t.Results ([]blob.ListBlobItem) (slice) + case "results": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Results: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Results: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Results: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]ListBlobItem, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Results = append(t.Results, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Results: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Results: slice too large") + } + } + } + + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Size: %w", err) + } + t.Size = uint64(nval) + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListOK") + } + } + } + + return nil +} +func (t *ListBlobItem) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return fmt.Errorf("string in field \"blob\" was too long") + } + if err := jw.WriteString(string("blob")); err != nil { + return fmt.Errorf("writing string for field \"blob\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Blob.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Blob: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.InsertedAt (int64) (int64) + if len("insertedAt") > 8192 { + return fmt.Errorf("string in field \"insertedAt\" was too long") + } + if err := jw.WriteString(string("insertedAt")); err != nil { + return fmt.Errorf("writing string for field \"insertedAt\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteInt64(int64(t.InsertedAt)); err != nil { + return fmt.Errorf("writing int64 for field t.InsertedAt: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListBlobItem) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListBlobItem{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListBlobItem: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListBlobItem: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListBlobItem: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListBlobItem: string too large") + } + return fmt.Errorf("reading string for field ListBlobItem: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListBlobItem: %w", err) + } + switch name { + + // t.Blob (blob.Blob) (struct) + case "blob": + + if err := t.Blob.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Blob: %w", err) + } + + // t.InsertedAt (int64) (int64) + case "insertedAt": + { + + nval, err := jr.ReadNumberAsInt64() + if err != nil { + return fmt.Errorf("reading int64 for field t.InsertedAt: %w", err) + } + t.InsertedAt = int64(nval) + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListBlobItem: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListBlobItem: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListBlobItem") + } + } + } + + return nil +} +func (t *RetrieveArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Blob (blob.RetrieveBlob) (struct) + if len("blob") > 8192 { + return fmt.Errorf("string in field \"blob\" was too long") + } + if err := jw.WriteString(string("blob")); err != nil { + return fmt.Errorf("writing string for field \"blob\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Blob.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Blob: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RetrieveArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RetrieveArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RetrieveArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RetrieveArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RetrieveArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RetrieveArguments: string too large") + } + return fmt.Errorf("reading string for field RetrieveArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RetrieveArguments: %w", err) + } + switch name { + + // t.Blob (blob.RetrieveBlob) (struct) + case "blob": + + if err := t.Blob.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Blob: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RetrieveArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RetrieveArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RetrieveArguments") + } + } + } + + return nil +} +func (t *RetrieveBlob) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RetrieveBlob) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RetrieveBlob{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RetrieveBlob: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RetrieveBlob: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RetrieveBlob: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RetrieveBlob: string too large") + } + return fmt.Errorf("reading string for field RetrieveBlob: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RetrieveBlob: %w", err) + } + switch name { + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RetrieveBlob: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RetrieveBlob: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RetrieveBlob") + } + } + } + + return nil +} +func (t *RetrieveOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RetrieveOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RetrieveOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RetrieveOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RetrieveOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RetrieveOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RetrieveOK: string too large") + } + return fmt.Errorf("reading string for field RetrieveOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RetrieveOK: %w", err) + } + switch name { + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RetrieveOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RetrieveOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RetrieveOK") + } + } + } + + return nil +} diff --git a/protocol/commands/blob/list.go b/protocol/commands/blob/list.go new file mode 100644 index 00000000..5df28a4e --- /dev/null +++ b/protocol/commands/blob/list.go @@ -0,0 +1,10 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +var List = binding.Bind[*ListArguments, *ListOK](command.MustParse("/blob/list")) diff --git a/protocol/commands/blob/reject.go b/protocol/commands/blob/reject.go new file mode 100644 index 00000000..71afe2cf --- /dev/null +++ b/protocol/commands/blob/reject.go @@ -0,0 +1,42 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/ucan/command" +) + +type RejectOK = commands.Unit + +// Reject (/blob/reject) is the "don't accept" inverse of `/blob/accept`: it +// retires a PARKED blob — allocated, never accepted, whether or not its +// bytes were ever received. A blob's lifecycle on a storage node ends in +// exactly one of `/blob/accept` (commit: aggregation, location claim, +// registration) or `/blob/reject` (drop: allocation released, bytes deleted). +// +// Served by storage nodes (subject = the provider DID, invoked by the +// upload service under its registration delegation, typically translating a +// client `/blob/abort`). The node drops the space's allocation and deletes +// any received bytes once no space holds an allocation or acceptance for +// the digest. +// +// A blob that THE INVOKING SPACE has accepted is refused with BlobAccepted — +// a space's accepted blobs are released via `/blob/remove`, never rejected. +// The guard is scoped to the invoking space, not the digest: another space's +// acceptance of the same bytes must not block the reject — the node simply +// drops this space's allocation and retains the bytes for the space that +// still claims them. +// +// Idempotent: rejecting an unknown or already-rejected blob succeeds. +// The receipt carries no payload (Unit). +var Reject = binding.Bind[*RejectArguments, *RejectOK](command.MustParse("/blob/reject")) + +// BlobAcceptedErrorName is the stable receipt-failure name when reject is +// invoked for a blob the invoking space has accepted — accepted blobs are +// released via `/blob/remove`, never rejected. +const BlobAcceptedErrorName = "BlobAccepted" + +var ErrBlobAccepted = errors.New(BlobAcceptedErrorName, "blob has been accepted by the space; release the claim via /blob/remove") diff --git a/protocol/commands/blob/release.go b/protocol/commands/blob/release.go new file mode 100644 index 00000000..943e4f9d --- /dev/null +++ b/protocol/commands/blob/release.go @@ -0,0 +1,50 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/ucan/command" +) + +type ReleaseOK = commands.Unit + +// Release (/blob/release) is the claim-release inverse of `/blob/allocate`: +// it drops one space's reference to a blob on a storage node. It is the +// upload service's translation of a client `/blob/remove`. +// +// Served by storage nodes (subject = the provider DID, invoked by the upload +// service under its registration delegation; the space travels in the +// arguments, matching Allocate/Accept). The node drops the space's +// allocation, acceptance and location claim. Bytes are physically deleted +// only when no space claims the digest anymore — and an accepted blob's +// bytes may additionally be retained until its PDP aggregate root is fully +// retired on-chain. Physical deletion is always asynchronous: the removal +// machinery re-verifies zero claims before every destructive step. +// +// The release carries Cause, a link to the originating `/blob/remove` task, +// and the remove invocation itself MUST ride in the request container so the +// node can verify the release is legitimate rather than trusting the upload +// service: the node rejects releases whose cause invocation is absent or not +// a `/blob/remove` task (UnknownCause), or whose subject/digest do not match +// the release's Space/Digest (InvalidCause). +// +// Idempotent: releasing an unknown or already-released blob succeeds. The +// receipt carries no payload (Unit). +var Release = binding.Bind[*ReleaseArguments, *ReleaseOK](command.MustParse("/blob/release")) + +// UnknownCauseErrorName is the stable receipt-failure name when a release's +// Cause is undefined, the linked invocation is not present in the request +// container, or the linked invocation is not a `/blob/remove` task. +const UnknownCauseErrorName = "UnknownCause" + +var ErrUnknownCause = errors.New(UnknownCauseErrorName, "unknown cause invocation") + +// InvalidCauseErrorName is the stable receipt-failure name when the linked +// `/blob/remove` task's subject does not equal Space or its digest does not +// equal Digest. +const InvalidCauseErrorName = "InvalidCause" + +var ErrInvalidCause = errors.New(InvalidCauseErrorName, "cause invocation does not match release arguments") diff --git a/protocol/commands/blob/remove.go b/protocol/commands/blob/remove.go new file mode 100644 index 00000000..a9e05260 --- /dev/null +++ b/protocol/commands/blob/remove.go @@ -0,0 +1,28 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type RemoveOK = commands.Unit + +// Remove (/blob/remove) releases a space's claim on an ACCEPTED blob. +// +// Served by the upload service (subject = the space). The service validates +// the caller's space authority, recovers every storage node holding the blob +// (the primary via the registration's receipt chain, plus any non-failed +// replicas), forwards a `/blob/release` to each, and deregisters the blob +// last — so the receipt chain to the primary survives for a retry if every +// forward fails. Forwarding is best-effort: the node-side handler is +// idempotent and unclaimed allocations expire, so a missed node is +// reconciled by provider-side hygiene. +// +// Parked (never-accepted) blobs are abandoned via `/blob/abort` instead. +// +// Idempotent: removing an unknown or already-removed blob succeeds. The +// receipt carries no payload (Unit). +var Remove = binding.Bind[*RemoveArguments, *RemoveOK](command.MustParse("/blob/remove")) diff --git a/protocol/commands/blob/replica/allocate.go b/protocol/commands/blob/replica/allocate.go new file mode 100644 index 00000000..102501a6 --- /dev/null +++ b/protocol/commands/blob/replica/allocate.go @@ -0,0 +1,10 @@ +//go:build !codegen + +package replica + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +var Allocate = binding.Bind[*AllocateArguments, *AllocateOK](command.MustParse("/blob/replica/allocate")) diff --git a/protocol/commands/blob/replica/cbor_gen.go b/protocol/commands/blob/replica/cbor_gen.go new file mode 100644 index 00000000..04fc07e5 --- /dev/null +++ b/protocol/commands/blob/replica/cbor_gen.go @@ -0,0 +1,538 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package replica + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *AllocateArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + + // t.Site (cid.Cid) (struct) + if len("site") > 8192 { + return xerrors.Errorf("Value in field \"site\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("site"))); err != nil { + return err + } + if _, err := cw.WriteString(string("site")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Site); err != nil { + return xerrors.Errorf("failed to write cid field t.Site: %w", err) + } + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return xerrors.Errorf("Value in field \"cause\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cause"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cause")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Cause); err != nil { + return xerrors.Errorf("failed to write cid field t.Cause: %w", err) + } + + return nil +} + +func (t *AllocateArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AllocateArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AllocateArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (blob.Blob) (struct) + case "blob": + + { + + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob: %w", err) + } + + } + // t.Site (cid.Cid) (struct) + case "site": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Site: %w", err) + } + + t.Site = c + + } + // t.Cause (cid.Cid) (struct) + case "cause": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Cause: %w", err) + } + + t.Cause = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AllocateOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Site (promise.AwaitOK) (struct) + if len("site") > 8192 { + return xerrors.Errorf("Value in field \"site\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("site"))); err != nil { + return err + } + if _, err := cw.WriteString(string("site")); err != nil { + return err + } + + if err := t.Site.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AllocateOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = AllocateOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AllocateOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Site (promise.AwaitOK) (struct) + case "site": + + { + + if err := t.Site.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Site: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *TransferArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.Blob (blob.Blob) (struct) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + + // t.Site (cid.Cid) (struct) + if len("site") > 8192 { + return xerrors.Errorf("Value in field \"site\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("site"))); err != nil { + return err + } + if _, err := cw.WriteString(string("site")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Site); err != nil { + return xerrors.Errorf("failed to write cid field t.Site: %w", err) + } + + // t.Cause (cid.Cid) (struct) + if len("cause") > 8192 { + return xerrors.Errorf("Value in field \"cause\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cause"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cause")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Cause); err != nil { + return xerrors.Errorf("failed to write cid field t.Cause: %w", err) + } + + return nil +} + +func (t *TransferArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = TransferArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("TransferArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (blob.Blob) (struct) + case "blob": + + { + + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob: %w", err) + } + + } + // t.Site (cid.Cid) (struct) + case "site": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Site: %w", err) + } + + t.Site = c + + } + // t.Cause (cid.Cid) (struct) + case "cause": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Cause: %w", err) + } + + t.Cause = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *TransferOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.PDP (promise.AwaitOK) (struct) + if len("pdp") > 8192 { + return xerrors.Errorf("Value in field \"pdp\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pdp"))); err != nil { + return err + } + if _, err := cw.WriteString(string("pdp")); err != nil { + return err + } + + if err := t.PDP.MarshalCBOR(cw); err != nil { + return err + } + + // t.Site (cid.Cid) (struct) + if len("site") > 8192 { + return xerrors.Errorf("Value in field \"site\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("site"))); err != nil { + return err + } + if _, err := cw.WriteString(string("site")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Site); err != nil { + return xerrors.Errorf("failed to write cid field t.Site: %w", err) + } + + return nil +} + +func (t *TransferOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = TransferOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("TransferOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.PDP (promise.AwaitOK) (struct) + case "pdp": + + { + + if err := t.PDP.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.PDP: %w", err) + } + + } + // t.Site (cid.Cid) (struct) + case "site": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Site: %w", err) + } + + t.Site = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/blob/replica/gen/main.go b/protocol/commands/blob/replica/gen/main.go new file mode 100644 index 00000000..10ccdb01 --- /dev/null +++ b/protocol/commands/blob/replica/gen/main.go @@ -0,0 +1,35 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + "github.com/fil-forge/forge/protocol/commands/blob/replica" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0o644); err != nil { + panic(err) + } +} + +func main() { + const cborFile = "../cbor_gen.go" + if err := cbg.WriteMapEncodersToFile(cborFile, "replica", + replica.AllocateArguments{}, + replica.AllocateOK{}, + replica.TransferArguments{}, + replica.TransferOK{}, + ); err != nil { + panic(err) + } + tag(cborFile) +} diff --git a/protocol/commands/blob/replica/transfer.go b/protocol/commands/blob/replica/transfer.go new file mode 100644 index 00000000..09deb750 --- /dev/null +++ b/protocol/commands/blob/replica/transfer.go @@ -0,0 +1,10 @@ +//go:build !codegen + +package replica + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +var Transfer = binding.Bind[*TransferArguments, *TransferOK](command.MustParse("/blob/replica/transfer")) diff --git a/protocol/commands/blob/replica/types.go b/protocol/commands/blob/replica/types.go new file mode 100644 index 00000000..145b3547 --- /dev/null +++ b/protocol/commands/blob/replica/types.go @@ -0,0 +1,43 @@ +package replica + +import ( + "github.com/fil-forge/ucantone/ucan/promise" + "github.com/ipfs/go-cid" + + "github.com/fil-forge/forge/protocol/commands/blob" +) + +type AllocateArguments struct { + // Blob is the blob to be allocated. + Blob blob.Blob `cborgen:"blob"` + // Site is a link to a location commitment indicating where the Blob must be + // fetched from. + Site cid.Cid `cborgen:"site"` + // Cause is a link to the `/blob/replicate` task that caused this allocation. + Cause cid.Cid `cborgen:"cause"` +} + +type AllocateOK struct { + // Site resolves to an additional location for the blob. + // It is a link to a /blob/replica/transfer task. + Site promise.AwaitOK `cborgen:"site"` +} + +type TransferArguments struct { + // Blob is the blob to be transferred. + Blob blob.Blob `cborgen:"blob"` + // Site is a link to a location commitment indicating where the Blob must be + // fetched from. + Site cid.Cid `cborgen:"site"` + // Cause links to the `/blob/replica/allocate` task that initiated this transfer. + Cause cid.Cid `cborgen:"cause"` +} + +type TransferOK struct { + // Site links to the location commitment that indicate where the Blob has been + // transferred to. + Site cid.Cid `cborgen:"site"` + // PDP links to the /pdp/accept task that will resolve when aggregation + // is complete and the piece is accepted. + PDP promise.AwaitOK `cborgen:"pdp"` +} diff --git a/protocol/commands/blob/replicate.go b/protocol/commands/blob/replicate.go new file mode 100644 index 00000000..9a032563 --- /dev/null +++ b/protocol/commands/blob/replicate.go @@ -0,0 +1,24 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// Replicate is a capability that allows an agent to replicate a Blob into a +// space identified by did:key in the `with` field. +// +// A Replicate capability may only be invoked after a `/blob/accept` receipt has +// been receieved, indicating the source node has successfully received the blob. +// Each Replicate task MUST target a different node, and they MUST NOT target +// the original upload target. +// +// The Replicate task receipt includes async tasks for `/blob/replica/allocate` +// and `/blob/replica/transfer`. Successful completion of the +// `/blob/replica/transfer` task indicates the replication target has +// transferred and stored the blob. The number of `/blob/replica/allocate` and +// `/blob/replica/transfer` tasks corresponds directly to number of replicas +// requested. +var Replicate = binding.Bind[*ReplicateArguments, *ReplicateOK](command.MustParse("/blob/replicate")) diff --git a/protocol/commands/blob/retrieve.go b/protocol/commands/blob/retrieve.go new file mode 100644 index 00000000..5004fd59 --- /dev/null +++ b/protocol/commands/blob/retrieve.go @@ -0,0 +1,18 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// Retrieve is the service-level retrieval capability (e.g. used by the +// indexer to fetch content claims from a Piri node). It is NOT space-scoped: +// any holder of a valid delegation for `/blob/retrieve` may fetch the blob +// by digest, regardless of which space it was originally stored under. +// +// For user-facing retrieval that requires an allocation in a specific space +// see `commands/content.Retrieve` (the `/content/retrieve` +// capability). +var Retrieve = binding.Bind[*RetrieveArguments, *RetrieveOK](command.MustParse("/blob/retrieve")) diff --git a/protocol/commands/blob/types.go b/protocol/commands/blob/types.go new file mode 100644 index 00000000..159bccb2 --- /dev/null +++ b/protocol/commands/blob/types.go @@ -0,0 +1,153 @@ +package blob + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/did" + "github.com/fil-forge/ucantone/ucan/promise" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" +) + +type Blob struct { + Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` + Size uint64 `cborgen:"size" dagjsongen:"size"` +} + +type AddArguments struct { + Blob Blob `cborgen:"blob" dagjsongen:"blob"` +} + +type AddOK struct { + // Site is a promise of the `/blob/accept` task result, which contains a + // location claim for the blob, describing where it can be retrieved from. + Site promise.AwaitOK `cborgen:"site" dagjsongen:"site"` +} + +type AcceptArguments struct { + Space did.DID `cborgen:"space" dagjsongen:"space"` + Blob Blob `cborgen:"blob" dagjsongen:"blob"` + Put promise.AwaitOK `cborgen:"_put" dagjsongen:"_put"` +} + +type AcceptOK struct { + Site cid.Cid `cborgen:"site" dagjsongen:"site"` + // PDP is a promise of the `/pdp/accept` task result, which completes when + // the piece has been aggregated and root added to the node's PDP dataset. + PDP promise.AwaitOK `cborgen:"pdp" dagjsongen:"pdp"` +} + +type AllocateArguments struct { + Space did.DID `cborgen:"space" dagjsongen:"space"` + Blob Blob `cborgen:"blob" dagjsongen:"blob"` + Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` +} + +type AllocateOK struct { + Size uint64 `cborgen:"size" dagjsongen:"size"` + Address *BlobAddress `cborgen:"address,omitempty" dagjsongen:"address,omitempty"` +} + +type BlobAddress struct { + URL commands.CborURL `cborgen:"url" dagjsongen:"url"` + Headers map[string]string `cborgen:"headers" dagjsongen:"headers"` + Expires int64 `cborgen:"expires" dagjsongen:"expires"` +} + +type ListArguments struct { + Cursor *string `cborgen:"cursor,omitempty" dagjsongen:"cursor,omitempty"` + Size *uint64 `cborgen:"size,omitempty" dagjsongen:"size,omitempty"` +} + +type ListOK struct { + Cursor *string `cborgen:"cursor,omitempty" dagjsongen:"cursor,omitempty"` + Size uint64 `cborgen:"size" dagjsongen:"size"` + Results []ListBlobItem `cborgen:"results" dagjsongen:"results"` +} + +type ListBlobItem struct { + Blob Blob `cborgen:"blob" dagjsongen:"blob"` + InsertedAt int64 `cborgen:"insertedAt" dagjsongen:"insertedAt"` +} + +// RemoveArguments releases the invoking space's claim on the blob +// identified by Digest. The space is the invocation subject — it is not +// repeated in the arguments (compare ReleaseArguments, the provider-rooted +// leg, where the subject is the provider and the space must travel +// explicitly). +type RemoveArguments struct { + Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` +} + +// ReleaseArguments drops Space's claim on the blob identified by Digest on a +// storage node. Space is explicit (matching Allocate/Accept) because the +// invocation subject is the provider, and storage nodes key allocations and +// acceptances by (digest, space): release drops one space's claim, and the +// node performs physical deletion only once no space claims the digest at +// all. Cause proves the release originates from the space: it links the +// `/blob/remove` task the release translates. +type ReleaseArguments struct { + Space did.DID `cborgen:"space" dagjsongen:"space"` + Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` + // Cause is the link to the `/blob/remove` task this release translates. + // The linked invocation MUST be present in the request container; the + // node verifies its subject equals Space and its digest equals Digest + // before dropping the claim. + Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` +} + +// AbortArguments abandons the invoking space's in-flight upload of the +// parked (never-accepted) blob identified by Digest. The space is the +// invocation subject. Cause is the `/blob/add` task link: the upload +// service uses it to recover which storage node holds the parked blob — a +// parked blob has no registration or acceptance to look the node up by. +type AbortArguments struct { + Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` + Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` +} + +// RejectArguments drops Space's allocation for the parked (never-accepted) +// blob identified by Digest on the storage node; the node deletes any +// received bytes once no space holds an allocation or acceptance for the +// digest. +type RejectArguments struct { + Space did.DID `cborgen:"space" dagjsongen:"space"` + Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` +} + +type ReplicateArguments struct { + // Blob is the blob that must be replicated. + Blob Blob `cborgen:"blob" dagjsongen:"blob"` + // Replicas is the number of replicas to ensure. + // e.g. Replicas: 3 will ensure 3 copies of the data exist in a network in total. + Replicas uint64 `cborgen:"replicas" dagjsongen:"replicas"` + // Site is a link to a location commitment indicating where the Blob must be + // fetched from. + Site cid.Cid `cborgen:"site" dagjsongen:"site"` +} + +type ReplicateOK struct { + // Site resolves to additional locations for the blob. They are links to + // `/blob/replica/transfer` tasks. + Site []promise.AwaitOK `cborgen:"site" dagjsongen:"site"` +} + +// RetrieveBlob identifies a blob solely by its content multihash. Used by the +// service-level `/blob/retrieve` capability where the caller is fetching data +// by hash without prior knowledge of the byte size. +type RetrieveBlob struct { + Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` +} + +// RetrieveArguments is the argument shape of the `/blob/retrieve` capability — +// a service-level (not space-scoped) retrieval handle. Compare to +// `content.RetrieveArguments` which is space-scoped and carries a byte Range; +// the `/blob/retrieve` flow is consumed by service principals (e.g. the +// indexer) fetching content claims that aren't bound to any space. +type RetrieveArguments struct { + Blob RetrieveBlob `cborgen:"blob" dagjsongen:"blob"` +} + +// RetrieveOK is the success return for `/blob/retrieve`. The blob bytes +// themselves are streamed back through the response container's body (the +// retrieval package's HTTPHeader transport); the typed OK record is empty. +type RetrieveOK struct{} diff --git a/protocol/commands/cbor_gen.go b/protocol/commands/cbor_gen.go new file mode 100644 index 00000000..a2c785e5 --- /dev/null +++ b/protocol/commands/cbor_gen.go @@ -0,0 +1,88 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package commands + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *Unit) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{160}); err != nil { + return err + } + return nil +} + +func (t *Unit) UnmarshalCBOR(r io.Reader) (err error) { + *t = Unit{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Unit: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 0) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/cbor_url.go b/protocol/commands/cbor_url.go new file mode 100644 index 00000000..9393899d --- /dev/null +++ b/protocol/commands/cbor_url.go @@ -0,0 +1,99 @@ +package commands + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/url" + + jsg "github.com/alanshaw/dag-json-gen" + cbg "github.com/whyrusleeping/cbor-gen" +) + +var _ cbg.CBORMarshaler = (*CborURL)(nil) +var _ cbg.CBORUnmarshaler = (*CborURL)(nil) + +type CborURL url.URL + +func (cu CborURL) URL() *url.URL { + u := url.URL(cu) + return &u +} + +func (cu CborURL) MarshalCBOR(w io.Writer) error { + urlStr := cu.URL().String() + + if len(urlStr) > 8192 { + return errors.New("value in field cu.URL was too long") + } + + cw := cbg.NewCborWriter(w) + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(urlStr))); err != nil { + return err + } + + if _, err := cw.WriteString(urlStr); err != nil { + return err + } + + return nil +} + +func (cu *CborURL) UnmarshalCBOR(r io.Reader) error { + cr := cbg.NewCborReader(r) + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + parsed, err := url.Parse(sval) + if err != nil { + return err + } + + *(*url.URL)(cu) = *parsed + + return nil +} + +func (cu CborURL) MarshalDagJSON(w io.Writer) error { + urlStr := cu.URL().String() + if len(urlStr) > 8192 { + return errors.New("value in field cu.URL was too long") + } + buf, err := json.Marshal(urlStr) + if err != nil { + return err + } + _, err = w.Write(buf) + return err +} + +func (cu *CborURL) UnmarshalDagJSON(r io.Reader) error { + var urlStr string + jr := jsg.NewDagJsonReader(r) + urlStr, err := jr.ReadString(8192) + if err != nil { + return err + } + parsed, err := url.Parse(urlStr) + if err != nil { + return err + } + *(*url.URL)(cu) = *parsed + return nil +} + +func (cu CborURL) MarshalJSON() ([]byte, error) { + var b bytes.Buffer + if err := cu.MarshalDagJSON(&b); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +func (cu *CborURL) UnmarshalJSON(b []byte) error { + return cu.UnmarshalDagJSON(bytes.NewReader(b)) +} diff --git a/protocol/commands/claim/cache.go b/protocol/commands/claim/cache.go new file mode 100644 index 00000000..1df4bc9c --- /dev/null +++ b/protocol/commands/claim/cache.go @@ -0,0 +1,13 @@ +//go:build !codegen + +package claim + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type CacheOK = commands.Unit + +var Cache = binding.Bind[*CacheArguments, *CacheOK](command.MustParse("/claim/cache")) diff --git a/protocol/commands/claim/cbor_gen.go b/protocol/commands/claim/cbor_gen.go new file mode 100644 index 00000000..1be4306f --- /dev/null +++ b/protocol/commands/claim/cbor_gen.go @@ -0,0 +1,294 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package claim + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *CacheArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Claim (cid.Cid) (struct) + if len("claim") > 8192 { + return xerrors.Errorf("Value in field \"claim\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("claim"))); err != nil { + return err + } + if _, err := cw.WriteString(string("claim")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Claim); err != nil { + return xerrors.Errorf("failed to write cid field t.Claim: %w", err) + } + + // t.Provider (claim.Provider) (struct) + if len("provider") > 8192 { + return xerrors.Errorf("Value in field \"provider\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("provider"))); err != nil { + return err + } + if _, err := cw.WriteString(string("provider")); err != nil { + return err + } + + if err := t.Provider.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *CacheArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = CacheArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("CacheArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 8) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Claim (cid.Cid) (struct) + case "claim": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Claim: %w", err) + } + + t.Claim = c + + } + // t.Provider (claim.Provider) (struct) + case "provider": + + { + + if err := t.Provider.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Provider: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *Provider) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Addresses ([][]uint8) (slice) + if len("addresses") > 8192 { + return xerrors.Errorf("Value in field \"addresses\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("addresses"))); err != nil { + return err + } + if _, err := cw.WriteString(string("addresses")); err != nil { + return err + } + + if len(t.Addresses) > 8192 { + return xerrors.Errorf("Slice value in field t.Addresses was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Addresses))); err != nil { + return err + } + for _, v := range t.Addresses { + if len(v) > 2097152 { + return xerrors.Errorf("Byte array in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(v))); err != nil { + return err + } + + if _, err := cw.Write(v); err != nil { + return err + } + + } + return nil +} + +func (t *Provider) UnmarshalCBOR(r io.Reader) (err error) { + *t = Provider{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Provider: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Addresses ([][]uint8) (slice) + case "addresses": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Addresses: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Addresses = make([][]uint8, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Addresses[i]: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Addresses[i] = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Addresses[i]); err != nil { + return err + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/claim/gen/main.go b/protocol/commands/claim/gen/main.go new file mode 100644 index 00000000..0eb2050b --- /dev/null +++ b/protocol/commands/claim/gen/main.go @@ -0,0 +1,42 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/claim" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + claim.CacheArguments{}, + claim.Provider{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "claim", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "claim", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/claim/json_gen.go b/protocol/commands/claim/json_gen.go new file mode 100644 index 00000000..a165794c --- /dev/null +++ b/protocol/commands/claim/json_gen.go @@ -0,0 +1,305 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package claim + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *CacheArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Claim (cid.Cid) (struct) + if len("claim") > 8192 { + return fmt.Errorf("string in field \"claim\" was too long") + } + if err := jw.WriteString(string("claim")); err != nil { + return fmt.Errorf("writing string for field \"claim\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Claim); err != nil { + return fmt.Errorf("writing CID for field t.Claim: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Provider (claim.Provider) (struct) + if len("provider") > 8192 { + return fmt.Errorf("string in field \"provider\" was too long") + } + if err := jw.WriteString(string("provider")); err != nil { + return fmt.Errorf("writing string for field \"provider\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Provider.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Provider: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *CacheArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = CacheArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for CacheArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for CacheArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for CacheArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field CacheArguments: string too large") + } + return fmt.Errorf("reading string for field CacheArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field CacheArguments: %w", err) + } + switch name { + + // t.Claim (cid.Cid) (struct) + case "claim": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Claim: %w", err) + } + t.Claim = c + + } + + // t.Provider (claim.Provider) (struct) + case "provider": + + if err := t.Provider.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Provider: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for CacheArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field CacheArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for CacheArguments") + } + } + } + + return nil +} +func (t *Provider) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Addresses ([][]uint8) (slice) + if len("addresses") > 8192 { + return fmt.Errorf("string in field \"addresses\" was too long") + } + if err := jw.WriteString(string("addresses")); err != nil { + return fmt.Errorf("writing string for field \"addresses\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Addresses) > 8192 { + return fmt.Errorf("slice value in field t.Addresses was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Addresses: %w", err) + } + for i, v := range t.Addresses { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Addresses: %w", err) + } + } + if len(v) > 2097152 { + return fmt.Errorf("byte array in field v was too long") + } + + if err := jw.WriteBytes(v); err != nil { + return fmt.Errorf("writing bytes for field v: %w", err) + } + + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Addresses: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *Provider) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Provider{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for Provider: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for Provider: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for Provider: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field Provider: string too large") + } + return fmt.Errorf("reading string for field Provider: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field Provider: %w", err) + } + switch name { + + // t.Addresses ([][]uint8) (slice) + case "addresses": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Addresses: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Addresses: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Addresses: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([][]uint8, 1) + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field item[0]: byte array too large") + } + return fmt.Errorf("reading bytes for field item[0]: %w", err) + } + if len(bval) > 0 { + item[0] = []uint8(bval) + } + } + + t.Addresses = append(t.Addresses, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Addresses: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Addresses: slice too large") + } + } + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for Provider: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field Provider: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for Provider") + } + } + } + + return nil +} diff --git a/protocol/commands/claim/types.go b/protocol/commands/claim/types.go new file mode 100644 index 00000000..7308c8f4 --- /dev/null +++ b/protocol/commands/claim/types.go @@ -0,0 +1,13 @@ +package claim + +import "github.com/ipfs/go-cid" + +type CacheArguments struct { + Claim cid.Cid `cborgen:"claim" dagjsongen:"claim"` + Provider Provider `cborgen:"provider" dagjsongen:"provider"` +} + +type Provider struct { + // Addresses are byte encoded multiaddrs of the provider. + Addresses [][]byte `cborgen:"addresses" dagjsongen:"addresses"` +} diff --git a/protocol/commands/content/cbor_gen.maps.go b/protocol/commands/content/cbor_gen.maps.go new file mode 100644 index 00000000..2411d212 --- /dev/null +++ b/protocol/commands/content/cbor_gen.maps.go @@ -0,0 +1,253 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package content + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *Blob) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + return nil +} + +func (t *Blob) UnmarshalCBOR(r io.Reader) (err error) { + *t = Blob{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Blob: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RetrieveArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Blob (content.Blob) (struct) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if err := t.Blob.MarshalCBOR(cw); err != nil { + return err + } + + // t.Range (content.Range) (struct) + if len("range") > 8192 { + return xerrors.Errorf("Value in field \"range\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("range"))); err != nil { + return err + } + if _, err := cw.WriteString(string("range")); err != nil { + return err + } + + if err := t.Range.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *RetrieveArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RetrieveArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RetrieveArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (content.Blob) (struct) + case "blob": + + { + + if err := t.Blob.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Blob: %w", err) + } + + } + // t.Range (content.Range) (struct) + case "range": + + { + + if err := t.Range.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Range: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/content/cbor_gen.tuples.go b/protocol/commands/content/cbor_gen.tuples.go new file mode 100644 index 00000000..5453bdea --- /dev/null +++ b/protocol/commands/content/cbor_gen.tuples.go @@ -0,0 +1,104 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package content + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +var lengthBufRange = []byte{130} + +func (t *Range) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufRange); err != nil { + return err + } + + // t.Start (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Start)); err != nil { + return err + } + + // t.End (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.End)); err != nil { + return err + } + + return nil +} + +func (t *Range) UnmarshalCBOR(r io.Reader) (err error) { + *t = Range{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 2 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Start (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Start = uint64(extra) + + } + // t.End (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.End = uint64(extra) + + } + return nil +} diff --git a/protocol/commands/content/gen/main.go b/protocol/commands/content/gen/main.go new file mode 100644 index 00000000..a53e021f --- /dev/null +++ b/protocol/commands/content/gen/main.go @@ -0,0 +1,55 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/content" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + mapModels := []any{ + content.Blob{}, + content.RetrieveArguments{}, + } + tupleModels := []any{ + content.Range{}, + } + const ( + cborTuples = "../cbor_gen.tuples.go" + cborMaps = "../cbor_gen.maps.go" + jsonTuples = "../json_gen.tuples.go" + jsonMaps = "../json_gen.maps.go" + ) + if err := cbg.WriteTupleEncodersToFile(cborTuples, "content", tupleModels...); err != nil { + panic(err) + } + if err := cbg.WriteMapEncodersToFile(cborMaps, "content", mapModels...); err != nil { + panic(err) + } + if err := jsg.WriteTupleEncodersToFile(jsonTuples, "content", tupleModels...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonMaps, "content", mapModels...); err != nil { + panic(err) + } + tag(cborTuples) + tag(cborMaps) + tag(jsonTuples) + tag(jsonMaps) +} diff --git a/protocol/commands/content/json_gen.maps.go b/protocol/commands/content/json_gen.maps.go new file mode 100644 index 00000000..1a178df5 --- /dev/null +++ b/protocol/commands/content/json_gen.maps.go @@ -0,0 +1,246 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package content + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *Blob) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *Blob) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Blob{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for Blob: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for Blob: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for Blob: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field Blob: string too large") + } + return fmt.Errorf("reading string for field Blob: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field Blob: %w", err) + } + switch name { + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for Blob: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field Blob: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for Blob") + } + } + } + + return nil +} +func (t *RetrieveArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Blob (content.Blob) (struct) + if len("blob") > 8192 { + return fmt.Errorf("string in field \"blob\" was too long") + } + if err := jw.WriteString(string("blob")); err != nil { + return fmt.Errorf("writing string for field \"blob\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Blob.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Blob: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Range (content.Range) (struct) + if len("range") > 8192 { + return fmt.Errorf("string in field \"range\" was too long") + } + if err := jw.WriteString(string("range")); err != nil { + return fmt.Errorf("writing string for field \"range\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Range.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Range: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RetrieveArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RetrieveArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RetrieveArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RetrieveArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RetrieveArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RetrieveArguments: string too large") + } + return fmt.Errorf("reading string for field RetrieveArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RetrieveArguments: %w", err) + } + switch name { + + // t.Blob (content.Blob) (struct) + case "blob": + + if err := t.Blob.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Blob: %w", err) + } + + // t.Range (content.Range) (struct) + case "range": + + if err := t.Range.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Range: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RetrieveArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RetrieveArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RetrieveArguments") + } + } + } + + return nil +} diff --git a/protocol/commands/content/json_gen.tuples.go b/protocol/commands/content/json_gen.tuples.go new file mode 100644 index 00000000..45d9d385 --- /dev/null +++ b/protocol/commands/content/json_gen.tuples.go @@ -0,0 +1,114 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package content + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *Range) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field Range: %w", err) + } + + // t.Start (uint64) (uint64) + + if err := jw.WriteUint64(uint64(t.Start)); err != nil { + return fmt.Errorf("writing uint64 for field t.Start: %w", err) + } + + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field End: %w", err) + } + + // t.End (uint64) (uint64) + + if err := jw.WriteUint64(uint64(t.End)); err != nil { + return fmt.Errorf("writing uint64 for field t.End: %w", err) + } + + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field Range: %w", err) + } + return nil +} + +func (t *Range) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Range{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field Range: %w", err) + } + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field Range: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field Range: %w", err) + } + } else { + + // t.Start (uint64) (uint64) + + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Start: %w", err) + } + t.Start = uint64(nval) + + } + { + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field Range: %w", err) + } + if close { + return fmt.Errorf("json input has too few fields 1 < 2") + } + } + + // t.End (uint64) (uint64) + + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.End: %w", err) + } + t.End = uint64(nval) + + } + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field Range: %w", err) + } + } + return nil +} diff --git a/protocol/commands/content/retrieve.go b/protocol/commands/content/retrieve.go new file mode 100644 index 00000000..07d4a514 --- /dev/null +++ b/protocol/commands/content/retrieve.go @@ -0,0 +1,13 @@ +//go:build !codegen + +package content + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type RetrieveOK = commands.Unit + +var Retrieve = binding.Bind[*RetrieveArguments, *RetrieveOK](command.MustParse("/content/retrieve")) diff --git a/protocol/commands/content/types.go b/protocol/commands/content/types.go new file mode 100644 index 00000000..cb310caf --- /dev/null +++ b/protocol/commands/content/types.go @@ -0,0 +1,17 @@ +package content + +import "github.com/multiformats/go-multihash" + +type Blob struct { + Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` +} + +type Range struct { + Start uint64 `cborgen:"start" dagjsongen:"start"` + End uint64 `cborgen:"end" dagjsongen:"end"` +} + +type RetrieveArguments struct { + Blob Blob `cborgen:"blob" dagjsongen:"blob"` + Range Range `cborgen:"range" dagjsongen:"range"` +} diff --git a/protocol/commands/customer/add.go b/protocol/commands/customer/add.go new file mode 100644 index 00000000..ce0d3cd5 --- /dev/null +++ b/protocol/commands/customer/add.go @@ -0,0 +1,13 @@ +//go:build !codegen + +package customer + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type AddOK = commands.Unit + +var Add = binding.Bind[*AddArguments, *AddOK](command.MustParse("/customer/add")) diff --git a/protocol/commands/customer/cbor_gen.go b/protocol/commands/customer/cbor_gen.go new file mode 100644 index 00000000..e45ceca2 --- /dev/null +++ b/protocol/commands/customer/cbor_gen.go @@ -0,0 +1,295 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package customer + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *AddArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 4 + + if t.ExternalAccount == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Details (map[string]string) (map) + if len("details") > 8192 { + return xerrors.Errorf("Value in field \"details\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("details"))); err != nil { + return err + } + if _, err := cw.WriteString(string("details")); err != nil { + return err + } + + { + if len(t.Details) > 4096 { + return xerrors.Errorf("cannot marshal t.Details map too large") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajMap, uint64(len(t.Details))); err != nil { + return err + } + + keys := make([]string, 0, len(t.Details)) + for k := range t.Details { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := t.Details[k] + + if len(k) > 8192 { + return xerrors.Errorf("Value in field k was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(k))); err != nil { + return err + } + if _, err := cw.WriteString(string(k)); err != nil { + return err + } + + if len(v) > 8192 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + + // t.Product (did.DID) (struct) + if len("product") > 8192 { + return xerrors.Errorf("Value in field \"product\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("product"))); err != nil { + return err + } + if _, err := cw.WriteString(string("product")); err != nil { + return err + } + + if err := t.Product.MarshalCBOR(cw); err != nil { + return err + } + + // t.Customer (did.DID) (struct) + if len("customer") > 8192 { + return xerrors.Errorf("Value in field \"customer\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("customer"))); err != nil { + return err + } + if _, err := cw.WriteString(string("customer")); err != nil { + return err + } + + if err := t.Customer.MarshalCBOR(cw); err != nil { + return err + } + + // t.ExternalAccount (string) (string) + if t.ExternalAccount != nil { + + if len("externalAccount") > 8192 { + return xerrors.Errorf("Value in field \"externalAccount\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("externalAccount"))); err != nil { + return err + } + if _, err := cw.WriteString(string("externalAccount")); err != nil { + return err + } + + if t.ExternalAccount == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.ExternalAccount) > 8192 { + return xerrors.Errorf("Value in field t.ExternalAccount was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.ExternalAccount))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.ExternalAccount)); err != nil { + return err + } + } + } + return nil +} + +func (t *AddArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AddArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AddArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 15) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Details (map[string]string) (map) + case "details": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajMap { + return fmt.Errorf("expected a map (major type 5)") + } + if extra > 4096 { + return fmt.Errorf("t.Details: map too large") + } + + t.Details = make(map[string]string, extra) + + for i, l := 0, int(extra); i < l; i++ { + + var k string + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + k = string(sval) + } + + var v string + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + v = string(sval) + } + + t.Details[k] = v + + } + // t.Product (did.DID) (struct) + case "product": + + { + + if err := t.Product.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Product: %w", err) + } + + } + // t.Customer (did.DID) (struct) + case "customer": + + { + + if err := t.Customer.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Customer: %w", err) + } + + } + // t.ExternalAccount (string) (string) + case "externalAccount": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.ExternalAccount = (*string)(&sval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/customer/gen/main.go b/protocol/commands/customer/gen/main.go new file mode 100644 index 00000000..299a3c63 --- /dev/null +++ b/protocol/commands/customer/gen/main.go @@ -0,0 +1,41 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/customer" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + customer.AddArguments{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "customer", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "customer", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/customer/json_gen.go b/protocol/commands/customer/json_gen.go new file mode 100644 index 00000000..92462b4b --- /dev/null +++ b/protocol/commands/customer/json_gen.go @@ -0,0 +1,303 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package customer + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *AddArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Customer (did.DID) (struct) + if len("customer") > 8192 { + return fmt.Errorf("string in field \"customer\" was too long") + } + if err := jw.WriteString(string("customer")); err != nil { + return fmt.Errorf("writing string for field \"customer\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Customer.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Customer: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Details (map[string]string) (map) + if len("details") > 8192 { + return fmt.Errorf("string in field \"details\" was too long") + } + if err := jw.WriteString(string("details")); err != nil { + return fmt.Errorf("writing string for field \"details\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + { + if len(t.Details) > 4096 { + return fmt.Errorf("cannot marshal t.Details map too large") + } + + if err := jw.WriteObjectOpen(); err != nil { + return fmt.Errorf("writing object open for field t.Details: %w", err) + } + + keys := make([]string, 0, len(t.Details)) + for k := range t.Details { + keys = append(keys, k) + } + sort.Strings(keys) + for i, k := range keys { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Details: %w", err) + } + } + v := t.Details[k] + if len(k) > 8192 { + return fmt.Errorf("string in field k was too long") + } + if err := jw.WriteString(string(k)); err != nil { + return fmt.Errorf("writing string for field k: %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return fmt.Errorf("writing colon for field t.Details: %w", err) + } + + if len(v) > 8192 { + return fmt.Errorf("string in field v was too long") + } + if err := jw.WriteString(string(v)); err != nil { + return fmt.Errorf("writing string for field v: %w", err) + } + } + if err := jw.WriteObjectClose(); err != nil { + return fmt.Errorf("writing object close for field t.Details: %w", err) + } + } + + written = true + if t.ExternalAccount != nil { + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + } + + // t.ExternalAccount (string) (string) + if t.ExternalAccount != nil { + if len("externalAccount") > 8192 { + return fmt.Errorf("string in field \"externalAccount\" was too long") + } + if err := jw.WriteString(string("externalAccount")); err != nil { + return fmt.Errorf("writing string for field \"externalAccount\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if t.ExternalAccount == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.ExternalAccount: %w", err) + } + } else { + if len(*t.ExternalAccount) > 8192 { + return fmt.Errorf("string in field t.ExternalAccount was too long") + } + if err := jw.WriteString(string(*t.ExternalAccount)); err != nil { + return fmt.Errorf("writing string for field t.ExternalAccount: %w", err) + } + } + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Product (did.DID) (struct) + if len("product") > 8192 { + return fmt.Errorf("string in field \"product\" was too long") + } + if err := jw.WriteString(string("product")); err != nil { + return fmt.Errorf("writing string for field \"product\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Product.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Product: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AddArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AddArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AddArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AddArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AddArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AddArguments: string too large") + } + return fmt.Errorf("reading string for field AddArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AddArguments: %w", err) + } + switch name { + + // t.Customer (did.DID) (struct) + case "customer": + + if err := t.Customer.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Customer: %w", err) + } + + // t.Details (map[string]string) (map) + case "details": + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for field t.Details: %w", err) + } + + t.Details = map[string]string{} + + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for field t.Details: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for field t.Details: %w", err) + } + } else { + for i, l := 0, 8192; i < l; i++ { + var k string + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field k: string too long") + } + return fmt.Errorf("reading string for field k: %w", err) + } + k = string(sval) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field t.Details: %w", err) + } + var v string + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field v: string too long") + } + return fmt.Errorf("reading string for field v: %w", err) + } + v = string(sval) + } + t.Details[k] = v + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field t.Details: %w", err) + } + if close { + break + } + } + } + + // t.ExternalAccount (string) (string) + case "externalAccount": + { + sval, err := jr.ReadStringOrNull(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string or null for field t.ExternalAccount: string too long") + } + return fmt.Errorf("reading string or null for field t.ExternalAccount: %w", err) + } + if sval != nil { + t.ExternalAccount = (*string)(sval) + } + } + + // t.Product (did.DID) (struct) + case "product": + + if err := t.Product.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Product: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AddArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AddArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AddArguments") + } + } + } + + return nil +} diff --git a/protocol/commands/customer/types.go b/protocol/commands/customer/types.go new file mode 100644 index 00000000..5c29857b --- /dev/null +++ b/protocol/commands/customer/types.go @@ -0,0 +1,17 @@ +package customer + +import ( + "github.com/fil-forge/ucantone/did" +) + +type AddArguments struct { + // DID of the customer account e.g. `did:mailto:agent` + Customer did.DID `cborgen:"customer" dagjsongen:"customer"` + // Opaque identifier representing an account in the payment system + // e.g. Stripe customer ID (stripe:cus_9s6XKzkNRiz8i3) + ExternalAccount *string `cborgen:"externalAccount,omitempty" dagjsongen:"externalAccount,omitempty"` + // Unique identifier of the product a.k.a plan. + Product did.DID `cborgen:"product" dagjsongen:"product"` + // Misc customer details + Details map[string]string `cborgen:"details" dagjsongen:"details"` +} diff --git a/protocol/commands/debug/cbor_gen.go b/protocol/commands/debug/cbor_gen.go new file mode 100644 index 00000000..9bdcda21 --- /dev/null +++ b/protocol/commands/debug/cbor_gen.go @@ -0,0 +1,122 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package debug + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *EchoArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Message (string) (string) + if len("message") > 8192 { + return xerrors.Errorf("Value in field \"message\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("message"))); err != nil { + return err + } + if _, err := cw.WriteString(string("message")); err != nil { + return err + } + + if len(t.Message) > 8192 { + return xerrors.Errorf("Value in field t.Message was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Message))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Message)); err != nil { + return err + } + return nil +} + +func (t *EchoArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = EchoArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("EchoArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Message (string) (string) + case "message": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Message = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/debug/echo.go b/protocol/commands/debug/echo.go new file mode 100644 index 00000000..9f19c934 --- /dev/null +++ b/protocol/commands/debug/echo.go @@ -0,0 +1,12 @@ +//go:build !codegen + +package debug + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type EchoOK = EchoArguments + +var Echo = binding.Bind[*EchoArguments, *EchoOK](command.MustParse("/debug/echo")) diff --git a/protocol/commands/debug/gen/main.go b/protocol/commands/debug/gen/main.go new file mode 100644 index 00000000..75cb2142 --- /dev/null +++ b/protocol/commands/debug/gen/main.go @@ -0,0 +1,41 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/debug" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + debug.EchoArguments{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "debug", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "debug", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/debug/json_gen.go b/protocol/commands/debug/json_gen.go new file mode 100644 index 00000000..a0c787a1 --- /dev/null +++ b/protocol/commands/debug/json_gen.go @@ -0,0 +1,121 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package debug + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *EchoArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Message (string) (string) + if len("message") > 8192 { + return fmt.Errorf("string in field \"message\" was too long") + } + if err := jw.WriteString(string("message")); err != nil { + return fmt.Errorf("writing string for field \"message\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Message) > 8192 { + return fmt.Errorf("string in field t.Message was too long") + } + if err := jw.WriteString(string(t.Message)); err != nil { + return fmt.Errorf("writing string for field t.Message: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *EchoArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = EchoArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for EchoArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for EchoArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for EchoArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field EchoArguments: string too large") + } + return fmt.Errorf("reading string for field EchoArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field EchoArguments: %w", err) + } + switch name { + + // t.Message (string) (string) + case "message": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.Message: string too long") + } + return fmt.Errorf("reading string for field t.Message: %w", err) + } + t.Message = string(sval) + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for EchoArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field EchoArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for EchoArguments") + } + } + } + + return nil +} diff --git a/protocol/commands/debug/types.go b/protocol/commands/debug/types.go new file mode 100644 index 00000000..5788fd0e --- /dev/null +++ b/protocol/commands/debug/types.go @@ -0,0 +1,5 @@ +package debug + +type EchoArguments struct { + Message string `cborgen:"message" dagjsongen:"message"` +} diff --git a/protocol/commands/gen/main.go b/protocol/commands/gen/main.go new file mode 100644 index 00000000..0322b676 --- /dev/null +++ b/protocol/commands/gen/main.go @@ -0,0 +1,41 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + commands.Unit{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "commands", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "commands", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/http/cbor_gen.go b/protocol/commands/http/cbor_gen.go new file mode 100644 index 00000000..da886582 --- /dev/null +++ b/protocol/commands/http/cbor_gen.go @@ -0,0 +1,140 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package http + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *PutArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Body (blob.Blob) (struct) + if len("body") > 8192 { + return xerrors.Errorf("Value in field \"body\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { + return err + } + if _, err := cw.WriteString(string("body")); err != nil { + return err + } + + if err := t.Body.MarshalCBOR(cw); err != nil { + return err + } + + // t.Destination (promise.AwaitOK) (struct) + if len("destination") > 8192 { + return xerrors.Errorf("Value in field \"destination\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("destination"))); err != nil { + return err + } + if _, err := cw.WriteString(string("destination")); err != nil { + return err + } + + if err := t.Destination.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *PutArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = PutArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("PutArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Body (blob.Blob) (struct) + case "body": + + { + + if err := t.Body.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Body: %w", err) + } + + } + // t.Destination (promise.AwaitOK) (struct) + case "destination": + + { + + if err := t.Destination.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Destination: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/http/gen/main.go b/protocol/commands/http/gen/main.go new file mode 100644 index 00000000..07593b91 --- /dev/null +++ b/protocol/commands/http/gen/main.go @@ -0,0 +1,41 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/http" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + http.PutArguments{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "http", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "http", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/http/json_gen.go b/protocol/commands/http/json_gen.go new file mode 100644 index 00000000..61d152bb --- /dev/null +++ b/protocol/commands/http/json_gen.go @@ -0,0 +1,141 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package http + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *PutArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Body (blob.Blob) (struct) + if len("body") > 8192 { + return fmt.Errorf("string in field \"body\" was too long") + } + if err := jw.WriteString(string("body")); err != nil { + return fmt.Errorf("writing string for field \"body\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Body.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Body: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Destination (promise.AwaitOK) (struct) + if len("destination") > 8192 { + return fmt.Errorf("string in field \"destination\" was too long") + } + if err := jw.WriteString(string("destination")); err != nil { + return fmt.Errorf("writing string for field \"destination\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Destination.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Destination: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *PutArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = PutArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for PutArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for PutArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for PutArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field PutArguments: string too large") + } + return fmt.Errorf("reading string for field PutArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field PutArguments: %w", err) + } + switch name { + + // t.Body (blob.Blob) (struct) + case "body": + + if err := t.Body.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Body: %w", err) + } + + // t.Destination (promise.AwaitOK) (struct) + case "destination": + + if err := t.Destination.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Destination: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for PutArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field PutArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for PutArguments") + } + } + } + + return nil +} diff --git a/protocol/commands/http/put.go b/protocol/commands/http/put.go new file mode 100644 index 00000000..2fcdee55 --- /dev/null +++ b/protocol/commands/http/put.go @@ -0,0 +1,13 @@ +//go:build !codegen + +package http + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type PutOK = commands.Unit + +var Put = binding.Bind[*PutArguments, *PutOK](command.MustParse("/http/put")) diff --git a/protocol/commands/http/types.go b/protocol/commands/http/types.go new file mode 100644 index 00000000..b32b3fdf --- /dev/null +++ b/protocol/commands/http/types.go @@ -0,0 +1,13 @@ +package http + +import ( + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/ucantone/ucan/promise" +) + +type PutArguments struct { + Body blob.Blob `cborgen:"body" dagjsongen:"body"` + // Destination is the promise that resolves to the upload destination + // where the blob should be PUT to. It is the result of a /blob/allocate task. + Destination promise.AwaitOK `cborgen:"destination" dagjsongen:"destination"` +} diff --git a/protocol/commands/index/add.go b/protocol/commands/index/add.go new file mode 100644 index 00000000..07924a1a --- /dev/null +++ b/protocol/commands/index/add.go @@ -0,0 +1,18 @@ +//go:build !codegen + +package index + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/ucan/command" +) + +type AddOK = commands.Unit + +var Add = binding.Bind[*AddArguments, *AddOK](command.MustParse("/index/add")) + +const IndexNotFoundErrorName = "IndexNotFound" + +var ErrIndexNotFound = errors.New(IndexNotFoundErrorName, "index not found in space") diff --git a/protocol/commands/index/cbor_gen.go b/protocol/commands/index/cbor_gen.go new file mode 100644 index 00000000..4591686e --- /dev/null +++ b/protocol/commands/index/cbor_gen.go @@ -0,0 +1,118 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package index + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *AddArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Index (cid.Cid) (struct) + if len("index") > 8192 { + return xerrors.Errorf("Value in field \"index\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("index"))); err != nil { + return err + } + if _, err := cw.WriteString(string("index")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Index); err != nil { + return xerrors.Errorf("failed to write cid field t.Index: %w", err) + } + + return nil +} + +func (t *AddArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AddArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AddArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Index (cid.Cid) (struct) + case "index": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Index: %w", err) + } + + t.Index = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/index/gen/main.go b/protocol/commands/index/gen/main.go new file mode 100644 index 00000000..fc47ac5e --- /dev/null +++ b/protocol/commands/index/gen/main.go @@ -0,0 +1,41 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/index" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + index.AddArguments{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "index", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "index", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/index/json_gen.go b/protocol/commands/index/json_gen.go new file mode 100644 index 00000000..1a6ba288 --- /dev/null +++ b/protocol/commands/index/json_gen.go @@ -0,0 +1,119 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package index + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *AddArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Index (cid.Cid) (struct) + if len("index") > 8192 { + return fmt.Errorf("string in field \"index\" was too long") + } + if err := jw.WriteString(string("index")); err != nil { + return fmt.Errorf("writing string for field \"index\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Index); err != nil { + return fmt.Errorf("writing CID for field t.Index: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AddArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AddArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AddArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AddArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AddArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AddArguments: string too large") + } + return fmt.Errorf("reading string for field AddArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AddArguments: %w", err) + } + switch name { + + // t.Index (cid.Cid) (struct) + case "index": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Index: %w", err) + } + t.Index = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AddArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AddArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AddArguments") + } + } + } + + return nil +} diff --git a/protocol/commands/index/types.go b/protocol/commands/index/types.go new file mode 100644 index 00000000..32e950bf --- /dev/null +++ b/protocol/commands/index/types.go @@ -0,0 +1,8 @@ +package index + +import "github.com/ipfs/go-cid" + +type AddArguments struct { + // Index is a link to the Content Archive (CAR) containing the index. + Index cid.Cid `cborgen:"index" dagjsongen:"index"` +} diff --git a/protocol/commands/json_gen.go b/protocol/commands/json_gen.go new file mode 100644 index 00000000..e2b0bbb8 --- /dev/null +++ b/protocol/commands/json_gen.go @@ -0,0 +1,91 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package commands + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *Unit) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *Unit) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Unit{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for Unit: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for Unit: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for Unit: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field Unit: string too large") + } + return fmt.Errorf("reading string for field Unit: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field Unit: %w", err) + } + switch name { + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for Unit: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field Unit: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for Unit") + } + } + } + + return nil +} diff --git a/protocol/commands/pdp/accept.go b/protocol/commands/pdp/accept.go new file mode 100644 index 00000000..2b740ed6 --- /dev/null +++ b/protocol/commands/pdp/accept.go @@ -0,0 +1,10 @@ +//go:build !codegen + +package pdp + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +var Accept = binding.Bind[*AcceptArguments, *AcceptOK](command.MustParse("/pdp/accept")) diff --git a/protocol/commands/pdp/cbor_gen.go b/protocol/commands/pdp/cbor_gen.go new file mode 100644 index 00000000..1146d33a --- /dev/null +++ b/protocol/commands/pdp/cbor_gen.go @@ -0,0 +1,679 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package pdp + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *AcceptArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Blob (multihash.Multihash) (slice) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if len(t.Blob) > 2097152 { + return xerrors.Errorf("Byte array in field t.Blob was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Blob))); err != nil { + return err + } + + if _, err := cw.Write(t.Blob); err != nil { + return err + } + + return nil +} + +func (t *AcceptArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AcceptArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AcceptArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (multihash.Multihash) (slice) + case "blob": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Blob: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Blob = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Blob); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AcceptOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.Piece (cid.Cid) (struct) + if len("piece") > 8192 { + return xerrors.Errorf("Value in field \"piece\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("piece"))); err != nil { + return err + } + if _, err := cw.WriteString(string("piece")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Piece); err != nil { + return xerrors.Errorf("failed to write cid field t.Piece: %w", err) + } + + // t.Aggregate (cid.Cid) (struct) + if len("aggregate") > 8192 { + return xerrors.Errorf("Value in field \"aggregate\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("aggregate"))); err != nil { + return err + } + if _, err := cw.WriteString(string("aggregate")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Aggregate); err != nil { + return xerrors.Errorf("failed to write cid field t.Aggregate: %w", err) + } + + // t.InclusionProof (merkletree.ProofData) (struct) + if len("inclusionProof") > 8192 { + return xerrors.Errorf("Value in field \"inclusionProof\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("inclusionProof"))); err != nil { + return err + } + if _, err := cw.WriteString(string("inclusionProof")); err != nil { + return err + } + + if err := t.InclusionProof.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AcceptOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = AcceptOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AcceptOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 14) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Piece (cid.Cid) (struct) + case "piece": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Piece: %w", err) + } + + t.Piece = c + + } + // t.Aggregate (cid.Cid) (struct) + case "aggregate": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Aggregate: %w", err) + } + + t.Aggregate = c + + } + // t.InclusionProof (merkletree.ProofData) (struct) + case "inclusionProof": + + { + + if err := t.InclusionProof.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.InclusionProof: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *InfoArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Blob (multihash.Multihash) (slice) + if len("blob") > 8192 { + return xerrors.Errorf("Value in field \"blob\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("blob"))); err != nil { + return err + } + if _, err := cw.WriteString(string("blob")); err != nil { + return err + } + + if len(t.Blob) > 2097152 { + return xerrors.Errorf("Byte array in field t.Blob was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Blob))); err != nil { + return err + } + + if _, err := cw.Write(t.Blob); err != nil { + return err + } + + return nil +} + +func (t *InfoArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = InfoArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("InfoArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Blob (multihash.Multihash) (slice) + case "blob": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Blob: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Blob = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Blob); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *InfoAcceptedAggregate) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Aggregate (cid.Cid) (struct) + if len("aggregate") > 8192 { + return xerrors.Errorf("Value in field \"aggregate\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("aggregate"))); err != nil { + return err + } + if _, err := cw.WriteString(string("aggregate")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Aggregate); err != nil { + return xerrors.Errorf("failed to write cid field t.Aggregate: %w", err) + } + + // t.InclusionProof (merkletree.ProofData) (struct) + if len("inclusionProof") > 8192 { + return xerrors.Errorf("Value in field \"inclusionProof\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("inclusionProof"))); err != nil { + return err + } + if _, err := cw.WriteString(string("inclusionProof")); err != nil { + return err + } + + if err := t.InclusionProof.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *InfoAcceptedAggregate) UnmarshalCBOR(r io.Reader) (err error) { + *t = InfoAcceptedAggregate{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("InfoAcceptedAggregate: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 14) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Aggregate (cid.Cid) (struct) + case "aggregate": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Aggregate: %w", err) + } + + t.Aggregate = c + + } + // t.InclusionProof (merkletree.ProofData) (struct) + case "inclusionProof": + + { + + if err := t.InclusionProof.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.InclusionProof: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *InfoOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Piece (cid.Cid) (struct) + if len("piece") > 8192 { + return xerrors.Errorf("Value in field \"piece\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("piece"))); err != nil { + return err + } + if _, err := cw.WriteString(string("piece")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Piece); err != nil { + return xerrors.Errorf("failed to write cid field t.Piece: %w", err) + } + + // t.Aggregates ([]pdp.InfoAcceptedAggregate) (slice) + if len("aggregates") > 8192 { + return xerrors.Errorf("Value in field \"aggregates\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("aggregates"))); err != nil { + return err + } + if _, err := cw.WriteString(string("aggregates")); err != nil { + return err + } + + if len(t.Aggregates) > 8192 { + return xerrors.Errorf("Slice value in field t.Aggregates was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Aggregates))); err != nil { + return err + } + for _, v := range t.Aggregates { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *InfoOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = InfoOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("InfoOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Piece (cid.Cid) (struct) + case "piece": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Piece: %w", err) + } + + t.Piece = c + + } + // t.Aggregates ([]pdp.InfoAcceptedAggregate) (slice) + case "aggregates": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Aggregates: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Aggregates = make([]InfoAcceptedAggregate, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Aggregates[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Aggregates[i]: %w", err) + } + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/pdp/gen/main.go b/protocol/commands/pdp/gen/main.go new file mode 100644 index 00000000..b8a2e4ea --- /dev/null +++ b/protocol/commands/pdp/gen/main.go @@ -0,0 +1,40 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/fil-forge/forge/protocol/commands/pdp" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + pdp.AcceptArguments{}, + pdp.AcceptOK{}, + pdp.InfoArguments{}, + pdp.InfoAcceptedAggregate{}, + pdp.InfoOK{}, + } + const cborFile = "../cbor_gen.go" + // merkletree.ProofData implements CBOR but not dag-json, and the stack is + // CBOR-only on the wire, so we don't generate dag-json codecs here. + if err := cbg.WriteMapEncodersToFile(cborFile, "pdp", models...); err != nil { + panic(err) + } + tag(cborFile) +} diff --git a/protocol/commands/pdp/info.go b/protocol/commands/pdp/info.go new file mode 100644 index 00000000..6aec637e --- /dev/null +++ b/protocol/commands/pdp/info.go @@ -0,0 +1,12 @@ +//go:build !codegen + +package pdp + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +const InfoCommand = "/pdp/info" + +var Info = binding.Bind[*InfoArguments, *InfoOK](command.MustParse(InfoCommand)) diff --git a/protocol/commands/pdp/sign/cbor_gen.go b/protocol/commands/pdp/sign/cbor_gen.go new file mode 100644 index 00000000..9921ed1c --- /dev/null +++ b/protocol/commands/pdp/sign/cbor_gen.go @@ -0,0 +1,1712 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package sign + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" + big "math/big" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *AuthSignature) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{166}); err != nil { + return err + } + + // t.R ([]uint8) (slice) + if len("r") > 8192 { + return xerrors.Errorf("Value in field \"r\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("r"))); err != nil { + return err + } + if _, err := cw.WriteString(string("r")); err != nil { + return err + } + + if len(t.R) > 2097152 { + return xerrors.Errorf("Byte array in field t.R was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.R))); err != nil { + return err + } + + if _, err := cw.Write(t.R); err != nil { + return err + } + + // t.S ([]uint8) (slice) + if len("s") > 8192 { + return xerrors.Errorf("Value in field \"s\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("s"))); err != nil { + return err + } + if _, err := cw.WriteString(string("s")); err != nil { + return err + } + + if len(t.S) > 2097152 { + return xerrors.Errorf("Byte array in field t.S was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.S))); err != nil { + return err + } + + if _, err := cw.Write(t.S); err != nil { + return err + } + + // t.V (uint8) (uint8) + if len("v") > 8192 { + return xerrors.Errorf("Value in field \"v\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("v"))); err != nil { + return err + } + if _, err := cw.WriteString(string("v")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.V)); err != nil { + return err + } + + // t.Signer ([]uint8) (slice) + if len("signer") > 8192 { + return xerrors.Errorf("Value in field \"signer\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("signer"))); err != nil { + return err + } + if _, err := cw.WriteString(string("signer")); err != nil { + return err + } + + if len(t.Signer) > 2097152 { + return xerrors.Errorf("Byte array in field t.Signer was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Signer))); err != nil { + return err + } + + if _, err := cw.Write(t.Signer); err != nil { + return err + } + + // t.Signature ([]uint8) (slice) + if len("signature") > 8192 { + return xerrors.Errorf("Value in field \"signature\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("signature"))); err != nil { + return err + } + if _, err := cw.WriteString(string("signature")); err != nil { + return err + } + + if len(t.Signature) > 2097152 { + return xerrors.Errorf("Byte array in field t.Signature was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Signature))); err != nil { + return err + } + + if _, err := cw.Write(t.Signature); err != nil { + return err + } + + // t.SignedData ([]uint8) (slice) + if len("signedData") > 8192 { + return xerrors.Errorf("Value in field \"signedData\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("signedData"))); err != nil { + return err + } + if _, err := cw.WriteString(string("signedData")); err != nil { + return err + } + + if len(t.SignedData) > 2097152 { + return xerrors.Errorf("Byte array in field t.SignedData was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.SignedData))); err != nil { + return err + } + + if _, err := cw.Write(t.SignedData); err != nil { + return err + } + + return nil +} + +func (t *AuthSignature) UnmarshalCBOR(r io.Reader) (err error) { + *t = AuthSignature{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AuthSignature: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 10) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.R ([]uint8) (slice) + case "r": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.R: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.R = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.R); err != nil { + return err + } + + // t.S ([]uint8) (slice) + case "s": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.S: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.S = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.S); err != nil { + return err + } + + // t.V (uint8) (uint8) + case "v": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint8 field") + } + if extra > math.MaxUint8 { + return fmt.Errorf("integer in input was too large for uint8 field") + } + t.V = uint8(extra) + // t.Signer ([]uint8) (slice) + case "signer": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Signer: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Signer = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Signer); err != nil { + return err + } + + // t.Signature ([]uint8) (slice) + case "signature": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Signature: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Signature = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Signature); err != nil { + return err + } + + // t.SignedData ([]uint8) (slice) + case "signedData": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.SignedData: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.SignedData = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.SignedData); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *Metadata) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Keys ([]string) (slice) + if len("keys") > 8192 { + return xerrors.Errorf("Value in field \"keys\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("keys"))); err != nil { + return err + } + if _, err := cw.WriteString(string("keys")); err != nil { + return err + } + + if len(t.Keys) > 8192 { + return xerrors.Errorf("Slice value in field t.Keys was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Keys))); err != nil { + return err + } + for _, v := range t.Keys { + if len(v) > 8192 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + + // t.Values (map[string]string) (map) + if len("values") > 8192 { + return xerrors.Errorf("Value in field \"values\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("values"))); err != nil { + return err + } + if _, err := cw.WriteString(string("values")); err != nil { + return err + } + + { + if len(t.Values) > 4096 { + return xerrors.Errorf("cannot marshal t.Values map too large") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajMap, uint64(len(t.Values))); err != nil { + return err + } + + keys := make([]string, 0, len(t.Values)) + for k := range t.Values { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := t.Values[k] + + if len(k) > 8192 { + return xerrors.Errorf("Value in field k was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(k))); err != nil { + return err + } + if _, err := cw.WriteString(string(k)); err != nil { + return err + } + + if len(v) > 8192 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + return nil +} + +func (t *Metadata) UnmarshalCBOR(r io.Reader) (err error) { + *t = Metadata{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Metadata: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Keys ([]string) (slice) + case "keys": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Keys: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Keys = make([]string, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Keys[i] = string(sval) + } + + } + } + // t.Values (map[string]string) (map) + case "values": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajMap { + return fmt.Errorf("expected a map (major type 5)") + } + if extra > 4096 { + return fmt.Errorf("t.Values: map too large") + } + + t.Values = make(map[string]string, extra) + + for i, l := 0, int(extra); i < l; i++ { + + var k string + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + k = string(sval) + } + + var v string + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + v = string(sval) + } + + t.Values[k] = v + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *PieceProofs) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Proofs ([]cid.Cid) (slice) + if len("proofs") > 8192 { + return xerrors.Errorf("Value in field \"proofs\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("proofs"))); err != nil { + return err + } + if _, err := cw.WriteString(string("proofs")); err != nil { + return err + } + + if len(t.Proofs) > 8192 { + return xerrors.Errorf("Slice value in field t.Proofs was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Proofs))); err != nil { + return err + } + for _, v := range t.Proofs { + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + return nil +} + +func (t *PieceProofs) UnmarshalCBOR(r io.Reader) (err error) { + *t = PieceProofs{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("PieceProofs: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Proofs ([]cid.Cid) (slice) + case "proofs": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Proofs: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Proofs = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Proofs[i]: %w", err) + } + + t.Proofs[i] = c + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *DataSetCreateArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.Payee ([]uint8) (slice) + if len("payee") > 8192 { + return xerrors.Errorf("Value in field \"payee\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("payee"))); err != nil { + return err + } + if _, err := cw.WriteString(string("payee")); err != nil { + return err + } + + if len(t.Payee) > 2097152 { + return xerrors.Errorf("Byte array in field t.Payee was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Payee))); err != nil { + return err + } + + if _, err := cw.Write(t.Payee); err != nil { + return err + } + + // t.DataSet (big.Int) (struct) + if len("dataSet") > 8192 { + return xerrors.Errorf("Value in field \"dataSet\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("dataSet"))); err != nil { + return err + } + if _, err := cw.WriteString(string("dataSet")); err != nil { + return err + } + + { + if t.DataSet != nil && t.DataSet.Sign() < 0 { + return xerrors.Errorf("Value in field t.DataSet was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.DataSet != nil { + b = t.DataSet.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + // t.Metadata (sign.Metadata) (struct) + if len("metadata") > 8192 { + return xerrors.Errorf("Value in field \"metadata\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("metadata"))); err != nil { + return err + } + if _, err := cw.WriteString(string("metadata")); err != nil { + return err + } + + if err := t.Metadata.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *DataSetCreateArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = DataSetCreateArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("DataSetCreateArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 8) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Payee ([]uint8) (slice) + case "payee": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Payee: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Payee = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Payee); err != nil { + return err + } + + // t.DataSet (big.Int) (struct) + case "dataSet": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.DataSet: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.DataSet = big.NewInt(0).SetBytes(buf) + } else { + t.DataSet = big.NewInt(0) + } + // t.Metadata (sign.Metadata) (struct) + case "metadata": + + { + + if err := t.Metadata.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Metadata: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *DataSetDeleteArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.DataSet (big.Int) (struct) + if len("dataSet") > 8192 { + return xerrors.Errorf("Value in field \"dataSet\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("dataSet"))); err != nil { + return err + } + if _, err := cw.WriteString(string("dataSet")); err != nil { + return err + } + + { + if t.DataSet != nil && t.DataSet.Sign() < 0 { + return xerrors.Errorf("Value in field t.DataSet was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.DataSet != nil { + b = t.DataSet.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + return nil +} + +func (t *DataSetDeleteArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = DataSetDeleteArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("DataSetDeleteArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.DataSet (big.Int) (struct) + case "dataSet": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.DataSet: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.DataSet = big.NewInt(0).SetBytes(buf) + } else { + t.DataSet = big.NewInt(0) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *PiecesAddArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{165}); err != nil { + return err + } + + // t.Nonce (big.Int) (struct) + if len("nonce") > 8192 { + return xerrors.Errorf("Value in field \"nonce\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("nonce"))); err != nil { + return err + } + if _, err := cw.WriteString(string("nonce")); err != nil { + return err + } + + { + if t.Nonce != nil && t.Nonce.Sign() < 0 { + return xerrors.Errorf("Value in field t.Nonce was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.Nonce != nil { + b = t.Nonce.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + // t.Proofs ([]sign.PieceProofs) (slice) + if len("proofs") > 8192 { + return xerrors.Errorf("Value in field \"proofs\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("proofs"))); err != nil { + return err + } + if _, err := cw.WriteString(string("proofs")); err != nil { + return err + } + + if len(t.Proofs) > 8192 { + return xerrors.Errorf("Slice value in field t.Proofs was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Proofs))); err != nil { + return err + } + for _, v := range t.Proofs { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + + // t.DataSet (big.Int) (struct) + if len("dataSet") > 8192 { + return xerrors.Errorf("Value in field \"dataSet\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("dataSet"))); err != nil { + return err + } + if _, err := cw.WriteString(string("dataSet")); err != nil { + return err + } + + { + if t.DataSet != nil && t.DataSet.Sign() < 0 { + return xerrors.Errorf("Value in field t.DataSet was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.DataSet != nil { + b = t.DataSet.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + // t.Metadata ([]sign.Metadata) (slice) + if len("metadata") > 8192 { + return xerrors.Errorf("Value in field \"metadata\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("metadata"))); err != nil { + return err + } + if _, err := cw.WriteString(string("metadata")); err != nil { + return err + } + + if len(t.Metadata) > 8192 { + return xerrors.Errorf("Slice value in field t.Metadata was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Metadata))); err != nil { + return err + } + for _, v := range t.Metadata { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + + // t.PieceData ([][]uint8) (slice) + if len("pieceData") > 8192 { + return xerrors.Errorf("Value in field \"pieceData\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pieceData"))); err != nil { + return err + } + if _, err := cw.WriteString(string("pieceData")); err != nil { + return err + } + + if len(t.PieceData) > 8192 { + return xerrors.Errorf("Slice value in field t.PieceData was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.PieceData))); err != nil { + return err + } + for _, v := range t.PieceData { + if len(v) > 2097152 { + return xerrors.Errorf("Byte array in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(v))); err != nil { + return err + } + + if _, err := cw.Write(v); err != nil { + return err + } + + } + return nil +} + +func (t *PiecesAddArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = PiecesAddArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("PiecesAddArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Nonce (big.Int) (struct) + case "nonce": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.Nonce: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.Nonce = big.NewInt(0).SetBytes(buf) + } else { + t.Nonce = big.NewInt(0) + } + // t.Proofs ([]sign.PieceProofs) (slice) + case "proofs": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Proofs: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Proofs = make([]PieceProofs, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Proofs[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Proofs[i]: %w", err) + } + + } + + } + } + // t.DataSet (big.Int) (struct) + case "dataSet": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.DataSet: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.DataSet = big.NewInt(0).SetBytes(buf) + } else { + t.DataSet = big.NewInt(0) + } + // t.Metadata ([]sign.Metadata) (slice) + case "metadata": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Metadata: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Metadata = make([]Metadata, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Metadata[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Metadata[i]: %w", err) + } + + } + + } + } + // t.PieceData ([][]uint8) (slice) + case "pieceData": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.PieceData: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.PieceData = make([][]uint8, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.PieceData[i]: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.PieceData[i] = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.PieceData[i]); err != nil { + return err + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *PiecesRemoveScheduleArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Pieces ([]*big.Int) (slice) + if len("pieces") > 8192 { + return xerrors.Errorf("Value in field \"pieces\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pieces"))); err != nil { + return err + } + if _, err := cw.WriteString(string("pieces")); err != nil { + return err + } + + if len(t.Pieces) > 8192 { + return xerrors.Errorf("Slice value in field t.Pieces was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Pieces))); err != nil { + return err + } + for _, v := range t.Pieces { + { + if v != nil && v.Sign() < 0 { + return xerrors.Errorf("Value in field v was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if v != nil { + b = v.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + + } + + // t.DataSet (big.Int) (struct) + if len("dataSet") > 8192 { + return xerrors.Errorf("Value in field \"dataSet\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("dataSet"))); err != nil { + return err + } + if _, err := cw.WriteString(string("dataSet")); err != nil { + return err + } + + { + if t.DataSet != nil && t.DataSet.Sign() < 0 { + return xerrors.Errorf("Value in field t.DataSet was a negative big-integer (not supported)") + } + if err := cw.CborWriteHeader(cbg.MajTag, 2); err != nil { + return err + } + var b []byte + if t.DataSet != nil { + b = t.DataSet.Bytes() + } + + if err := cw.CborWriteHeader(cbg.MajByteString, uint64(len(b))); err != nil { + return err + } + if _, err := cw.Write(b); err != nil { + return err + } + } + return nil +} + +func (t *PiecesRemoveScheduleArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = PiecesRemoveScheduleArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("PiecesRemoveScheduleArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Pieces ([]*big.Int) (slice) + case "pieces": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Pieces: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Pieces = make([]*big.Int, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.Pieces[i]: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.Pieces[i] = big.NewInt(0).SetBytes(buf) + } else { + t.Pieces[i] = big.NewInt(0) + } + + } + } + // t.DataSet (big.Int) (struct) + case "dataSet": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajTag || extra != 2 { + return fmt.Errorf("big ints should be cbor bignums") + } + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if maj != cbg.MajByteString { + return fmt.Errorf("big ints should be tagged cbor byte strings") + } + + if extra > 256 { + return fmt.Errorf("t.DataSet: cbor bignum was too large") + } + + if extra > 0 { + buf := make([]byte, extra) + if _, err := io.ReadFull(cr, buf); err != nil { + return err + } + t.DataSet = big.NewInt(0).SetBytes(buf) + } else { + t.DataSet = big.NewInt(0) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/pdp/sign/gen/main.go b/protocol/commands/pdp/sign/gen/main.go new file mode 100644 index 00000000..4a6dced1 --- /dev/null +++ b/protocol/commands/pdp/sign/gen/main.go @@ -0,0 +1,49 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + cbg "github.com/whyrusleeping/cbor-gen" + + "github.com/fil-forge/forge/protocol/commands/pdp/sign" +) + +// The parent sign package has bindcap.New calls that require codec methods +// to exist on the argument types. Those codecs are what this tool +// generates. To break the bootstrap, bindings and generated codec files +// carry `//go:build !codegen`; this tool is built with `-tags codegen`, so +// the import of `sign` here only pulls in the wire types from types.go. +// +// After cbor-gen writes the codec file, we re-tag it with the same +// constraint so subsequent codegen runs are stale-safe. +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + sign.AuthSignature{}, + sign.Metadata{}, + sign.PieceProofs{}, + sign.DataSetCreateArguments{}, + sign.DataSetDeleteArguments{}, + sign.PiecesAddArguments{}, + sign.PiecesRemoveScheduleArguments{}, + } + const cborFile = "../cbor_gen.go" + // The stack is CBOR-only on the wire, so we don't generate dag-json codecs. + if err := cbg.WriteMapEncodersToFile(cborFile, "sign", models...); err != nil { + panic(err) + } + tag(cborFile) +} diff --git a/protocol/commands/pdp/sign/sign.go b/protocol/commands/pdp/sign/sign.go new file mode 100644 index 00000000..800feee7 --- /dev/null +++ b/protocol/commands/pdp/sign/sign.go @@ -0,0 +1,24 @@ +//go:build !codegen + +package sign + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// Every /pdp/sign/* operation returns the same shape; these per-operation +// labels exist so call sites can keep the operation name in the type. +type ( + DataSetCreateOK = AuthSignature + DataSetDeleteOK = AuthSignature + PiecesAddOK = AuthSignature + PiecesRemoveScheduleOK = AuthSignature +) + +var ( + DataSetCreate = binding.Bind[*DataSetCreateArguments, *DataSetCreateOK](command.MustParse("/pdp/sign/dataset/create")) + DataSetDelete = binding.Bind[*DataSetDeleteArguments, *DataSetDeleteOK](command.MustParse("/pdp/sign/dataset/delete")) + PiecesAdd = binding.Bind[*PiecesAddArguments, *PiecesAddOK](command.MustParse("/pdp/sign/pieces/add")) + PiecesRemoveSchedule = binding.Bind[*PiecesRemoveScheduleArguments, *PiecesRemoveScheduleOK](command.MustParse("/pdp/sign/pieces/remove/schedule")) +) diff --git a/protocol/commands/pdp/sign/types.go b/protocol/commands/pdp/sign/types.go new file mode 100644 index 00000000..ef5cd2a5 --- /dev/null +++ b/protocol/commands/pdp/sign/types.go @@ -0,0 +1,88 @@ +// Package sign exposes the four /pdp/sign/* UCAN capabilities used by piri +// to drive an eip712 signing service: +// +// - /pdp/sign/dataset/create +// - /pdp/sign/dataset/delete +// - /pdp/sign/pieces/add +// - /pdp/sign/pieces/remove/schedule +// +// Each returns an [AuthSignature] (an eip712-signed bytes payload). +// +// Wire-level conventions: +// +// - `*big.Int` fields (DataSet, Nonce, Pieces) are encoded as CBOR +// bignums (tag 2). cbor-gen does not support negative bignums; the +// domain (dataset IDs, nonces, piece IDs) is non-negative. +// +// - `common.Address` (20 bytes) and `common.Hash` (32 bytes) are plain +// `[]byte`; length validation is left to the caller. +package sign + +import ( + "math/big" + + "github.com/ipfs/go-cid" +) + +// AuthSignature is the eip712 auth signature returned from every +// /pdp/sign/* invocation. Field semantics mirror +// filecoin-services/go/eip712.AuthSignature. +type AuthSignature struct { + Signature []byte `cborgen:"signature" dagjsongen:"signature"` + V uint8 `cborgen:"v" dagjsongen:"v"` + R []byte `cborgen:"r" dagjsongen:"r"` + S []byte `cborgen:"s" dagjsongen:"s"` + SignedData []byte `cborgen:"signedData" dagjsongen:"signedData"` + Signer []byte `cborgen:"signer" dagjsongen:"signer"` +} + +// Metadata is the eip712 metadata bag attached to dataset & piece signing +// requests. Keys carries insertion order so the wire encoding is +// deterministic across implementations. +type Metadata struct { + Keys []string `cborgen:"keys" dagjsongen:"keys"` + Values map[string]string `cborgen:"values" dagjsongen:"values"` +} + +// PieceProofs wraps the list of `blob/accept` invocation links proving the +// sub-pieces of one piece in a /pdp/sign/pieces/add request. cborgen does +// not natively encode `[][]cid.Cid`, so this single-level wrapper turns +// the outer slice into `[]PieceProofs`. +type PieceProofs struct { + Proofs []cid.Cid `cborgen:"proofs" dagjsongen:"proofs"` +} + +// DataSetCreateArguments are the arguments for /pdp/sign/dataset/create. +type DataSetCreateArguments struct { + DataSet *big.Int `cborgen:"dataSet" dagjsongen:"dataSet"` + Payee []byte `cborgen:"payee" dagjsongen:"payee"` + Metadata Metadata `cborgen:"metadata" dagjsongen:"metadata"` +} + +// DataSetDeleteArguments are the arguments for /pdp/sign/dataset/delete. +type DataSetDeleteArguments struct { + DataSet *big.Int `cborgen:"dataSet" dagjsongen:"dataSet"` +} + +// PiecesAddArguments are the arguments for /pdp/sign/pieces/add. +// +// Each entry in PieceData has a matching Metadata entry and a matching +// Proofs entry. The Proofs[i].Proofs list links to `blob/accept` +// invocations for every sub-piece of piece i. Each `blob/accept` receipt +// MUST include the corresponding `pdp/accept` effect receipt; all receipt +// data MUST be attached to the signing invocation's container as proof +// blocks. +type PiecesAddArguments struct { + DataSet *big.Int `cborgen:"dataSet" dagjsongen:"dataSet"` + Nonce *big.Int `cborgen:"nonce" dagjsongen:"nonce"` + PieceData [][]byte `cborgen:"pieceData" dagjsongen:"pieceData"` + Metadata []Metadata `cborgen:"metadata" dagjsongen:"metadata"` + Proofs []PieceProofs `cborgen:"proofs" dagjsongen:"proofs"` +} + +// PiecesRemoveScheduleArguments are the arguments for +// /pdp/sign/pieces/remove/schedule. +type PiecesRemoveScheduleArguments struct { + DataSet *big.Int `cborgen:"dataSet" dagjsongen:"dataSet"` + Pieces []*big.Int `cborgen:"pieces" dagjsongen:"pieces"` +} diff --git a/protocol/commands/pdp/types.go b/protocol/commands/pdp/types.go new file mode 100644 index 00000000..4c614b2c --- /dev/null +++ b/protocol/commands/pdp/types.go @@ -0,0 +1,31 @@ +package pdp + +import ( + "github.com/filecoin-project/go-data-segment/merkletree" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" +) + +type AcceptArguments struct { + Blob multihash.Multihash `cborgen:"blob" dagjsongen:"blob"` +} + +type AcceptOK struct { + Aggregate cid.Cid `cborgen:"aggregate" dagjsongen:"aggregate"` + InclusionProof merkletree.ProofData `cborgen:"inclusionProof" dagjsongen:"inclusionProof"` + Piece cid.Cid `cborgen:"piece" dagjsongen:"piece"` +} + +type InfoArguments struct { + Blob multihash.Multihash `cborgen:"blob" dagjsongen:"blob"` +} + +type InfoAcceptedAggregate struct { + Aggregate cid.Cid `cborgen:"aggregate" dagjsongen:"aggregate"` + InclusionProof merkletree.ProofData `cborgen:"inclusionProof" dagjsongen:"inclusionProof"` +} + +type InfoOK struct { + Piece cid.Cid `cborgen:"piece" dagjsongen:"piece"` + Aggregates []InfoAcceptedAggregate `cborgen:"aggregates" dagjsongen:"aggregates"` +} diff --git a/protocol/commands/provider/add.go b/protocol/commands/provider/add.go new file mode 100644 index 00000000..df40e623 --- /dev/null +++ b/protocol/commands/provider/add.go @@ -0,0 +1,18 @@ +//go:build !codegen + +package provider + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/ucan/command" +) + +var Add = binding.Bind[*AddArguments, *AddOK](command.MustParse("/provider/add")) + +const ( + InvalidAccountErrorName = "InvalidAccount" + AccountPlanMissingErrorName = "AccountPlanMissing" +) + +var ErrAccountPlanMissing = errors.New(AccountPlanMissingErrorName, "account does not have an active payment plan") diff --git a/protocol/commands/provider/cbor_gen.go b/protocol/commands/provider/cbor_gen.go new file mode 100644 index 00000000..c8de3bc6 --- /dev/null +++ b/protocol/commands/provider/cbor_gen.go @@ -0,0 +1,240 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package provider + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *AddArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Consumer (did.DID) (struct) + if len("consumer") > 8192 { + return xerrors.Errorf("Value in field \"consumer\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("consumer"))); err != nil { + return err + } + if _, err := cw.WriteString(string("consumer")); err != nil { + return err + } + + if err := t.Consumer.MarshalCBOR(cw); err != nil { + return err + } + + // t.Provider (did.DID) (struct) + if len("provider") > 8192 { + return xerrors.Errorf("Value in field \"provider\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("provider"))); err != nil { + return err + } + if _, err := cw.WriteString(string("provider")); err != nil { + return err + } + + if err := t.Provider.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AddArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AddArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AddArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 8) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Consumer (did.DID) (struct) + case "consumer": + + { + + if err := t.Consumer.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Consumer: %w", err) + } + + } + // t.Provider (did.DID) (struct) + case "provider": + + { + + if err := t.Provider.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Provider: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AddOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.ID (string) (string) + if len("id") > 8192 { + return xerrors.Errorf("Value in field \"id\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("id"))); err != nil { + return err + } + if _, err := cw.WriteString(string("id")); err != nil { + return err + } + + if len(t.ID) > 8192 { + return xerrors.Errorf("Value in field t.ID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.ID))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.ID)); err != nil { + return err + } + return nil +} + +func (t *AddOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = AddOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AddOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 2) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.ID (string) (string) + case "id": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.ID = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/provider/gen/main.go b/protocol/commands/provider/gen/main.go new file mode 100644 index 00000000..c4053e58 --- /dev/null +++ b/protocol/commands/provider/gen/main.go @@ -0,0 +1,42 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/provider" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + provider.AddArguments{}, + provider.AddOK{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "provider", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "provider", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/provider/json_gen.go b/protocol/commands/provider/json_gen.go new file mode 100644 index 00000000..fc1234e1 --- /dev/null +++ b/protocol/commands/provider/json_gen.go @@ -0,0 +1,240 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package provider + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *AddArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Consumer (did.DID) (struct) + if len("consumer") > 8192 { + return fmt.Errorf("string in field \"consumer\" was too long") + } + if err := jw.WriteString(string("consumer")); err != nil { + return fmt.Errorf("writing string for field \"consumer\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Consumer.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Consumer: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Provider (did.DID) (struct) + if len("provider") > 8192 { + return fmt.Errorf("string in field \"provider\" was too long") + } + if err := jw.WriteString(string("provider")); err != nil { + return fmt.Errorf("writing string for field \"provider\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Provider.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Provider: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AddArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AddArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AddArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AddArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AddArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AddArguments: string too large") + } + return fmt.Errorf("reading string for field AddArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AddArguments: %w", err) + } + switch name { + + // t.Consumer (did.DID) (struct) + case "consumer": + + if err := t.Consumer.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Consumer: %w", err) + } + + // t.Provider (did.DID) (struct) + case "provider": + + if err := t.Provider.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Provider: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AddArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AddArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AddArguments") + } + } + } + + return nil +} +func (t *AddOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.ID (string) (string) + if len("id") > 8192 { + return fmt.Errorf("string in field \"id\" was too long") + } + if err := jw.WriteString(string("id")); err != nil { + return fmt.Errorf("writing string for field \"id\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.ID) > 8192 { + return fmt.Errorf("string in field t.ID was too long") + } + if err := jw.WriteString(string(t.ID)); err != nil { + return fmt.Errorf("writing string for field t.ID: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AddOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AddOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AddOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AddOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AddOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AddOK: string too large") + } + return fmt.Errorf("reading string for field AddOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AddOK: %w", err) + } + switch name { + + // t.ID (string) (string) + case "id": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.ID: string too long") + } + return fmt.Errorf("reading string for field t.ID: %w", err) + } + t.ID = string(sval) + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AddOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AddOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AddOK") + } + } + } + + return nil +} diff --git a/protocol/commands/provider/types.go b/protocol/commands/provider/types.go new file mode 100644 index 00000000..f34744a9 --- /dev/null +++ b/protocol/commands/provider/types.go @@ -0,0 +1,12 @@ +package provider + +import "github.com/fil-forge/ucantone/did" + +type AddArguments struct { + Provider did.DID `cborgen:"provider" dagjsongen:"provider"` + Consumer did.DID `cborgen:"consumer" dagjsongen:"consumer"` +} + +type AddOK struct { + ID string `cborgen:"id" dagjsongen:"id"` +} diff --git a/protocol/commands/s3/bucket/cbor_gen.go b/protocol/commands/s3/bucket/cbor_gen.go new file mode 100644 index 00000000..cb7caff2 --- /dev/null +++ b/protocol/commands/s3/bucket/cbor_gen.go @@ -0,0 +1,1129 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package bucket + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *CreateArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Request (s3.Request) (struct) + if len("request") > 8192 { + return xerrors.Errorf("Value in field \"request\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("request"))); err != nil { + return err + } + if _, err := cw.WriteString(string("request")); err != nil { + return err + } + + if err := t.Request.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *CreateArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = CreateArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("CreateArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Request (s3.Request) (struct) + case "request": + + { + + if err := t.Request.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Request: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *DeleteArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Request (s3.Request) (struct) + if len("request") > 8192 { + return xerrors.Errorf("Value in field \"request\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("request"))); err != nil { + return err + } + if _, err := cw.WriteString(string("request")); err != nil { + return err + } + + if err := t.Request.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *DeleteArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = DeleteArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("DeleteArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Request (s3.Request) (struct) + case "request": + + { + + if err := t.Request.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Request: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ListArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Request (s3.Request) (struct) + if len("request") > 8192 { + return xerrors.Errorf("Value in field \"request\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("request"))); err != nil { + return err + } + if _, err := cw.WriteString(string("request")); err != nil { + return err + } + + if err := t.Request.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *ListArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Request (s3.Request) (struct) + case "request": + + { + + if err := t.Request.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Request: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *InfoArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Name (string) (string) + if len("name") > 8192 { + return xerrors.Errorf("Value in field \"name\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil { + return err + } + if _, err := cw.WriteString(string("name")); err != nil { + return err + } + + if len(t.Name) > 8192 { + return xerrors.Errorf("Value in field t.Name was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Name)); err != nil { + return err + } + + // t.AccessKey (did.DID) (struct) + if len("accessKey") > 8192 { + return xerrors.Errorf("Value in field \"accessKey\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("accessKey"))); err != nil { + return err + } + if _, err := cw.WriteString(string("accessKey")); err != nil { + return err + } + + if err := t.AccessKey.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *InfoArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = InfoArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("InfoArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Name (string) (string) + case "name": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Name = string(sval) + } + // t.AccessKey (did.DID) (struct) + case "accessKey": + + { + + if err := t.AccessKey.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.AccessKey: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *InfoOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.ID (did.DID) (struct) + if len("id") > 8192 { + return xerrors.Errorf("Value in field \"id\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("id"))); err != nil { + return err + } + if _, err := cw.WriteString(string("id")); err != nil { + return err + } + + if err := t.ID.MarshalCBOR(cw); err != nil { + return err + } + + // t.Delegations (s3.ProofSet) (struct) + if len("delegations") > 8192 { + return xerrors.Errorf("Value in field \"delegations\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("delegations"))); err != nil { + return err + } + if _, err := cw.WriteString(string("delegations")); err != nil { + return err + } + + if err := t.Delegations.MarshalCBOR(cw); err != nil { + return err + } + + // t.Permissions (s3.PermissionSet) (struct) + if len("permissions") > 8192 { + return xerrors.Errorf("Value in field \"permissions\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("permissions"))); err != nil { + return err + } + if _, err := cw.WriteString(string("permissions")); err != nil { + return err + } + + if err := t.Permissions.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *InfoOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = InfoOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("InfoOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.ID (did.DID) (struct) + case "id": + + { + + if err := t.ID.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.ID: %w", err) + } + + } + // t.Delegations (s3.ProofSet) (struct) + case "delegations": + + { + + if err := t.Delegations.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Delegations: %w", err) + } + + } + // t.Permissions (s3.PermissionSet) (struct) + case "permissions": + + { + + if err := t.Permissions.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Permissions: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ListOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{164}); err != nil { + return err + } + + // t.Owner (bucket.Owner) (struct) + if len("owner") > 8192 { + return xerrors.Errorf("Value in field \"owner\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("owner"))); err != nil { + return err + } + if _, err := cw.WriteString(string("owner")); err != nil { + return err + } + + if err := t.Owner.MarshalCBOR(cw); err != nil { + return err + } + + // t.Prefix (string) (string) + if len("prefix") > 8192 { + return xerrors.Errorf("Value in field \"prefix\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("prefix"))); err != nil { + return err + } + if _, err := cw.WriteString(string("prefix")); err != nil { + return err + } + + if len(t.Prefix) > 8192 { + return xerrors.Errorf("Value in field t.Prefix was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Prefix))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Prefix)); err != nil { + return err + } + + // t.Buckets ([]bucket.Bucket) (slice) + if len("buckets") > 8192 { + return xerrors.Errorf("Value in field \"buckets\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("buckets"))); err != nil { + return err + } + if _, err := cw.WriteString(string("buckets")); err != nil { + return err + } + + if len(t.Buckets) > 8192 { + return xerrors.Errorf("Slice value in field t.Buckets was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Buckets))); err != nil { + return err + } + for _, v := range t.Buckets { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + + // t.ContinuationToken (string) (string) + if len("continuationToken") > 8192 { + return xerrors.Errorf("Value in field \"continuationToken\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("continuationToken"))); err != nil { + return err + } + if _, err := cw.WriteString(string("continuationToken")); err != nil { + return err + } + + if len(t.ContinuationToken) > 8192 { + return xerrors.Errorf("Value in field t.ContinuationToken was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.ContinuationToken))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.ContinuationToken)); err != nil { + return err + } + return nil +} + +func (t *ListOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 17) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Owner (bucket.Owner) (struct) + case "owner": + + { + + if err := t.Owner.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Owner: %w", err) + } + + } + // t.Prefix (string) (string) + case "prefix": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Prefix = string(sval) + } + // t.Buckets ([]bucket.Bucket) (slice) + case "buckets": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Buckets: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Buckets = make([]Bucket, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Buckets[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Buckets[i]: %w", err) + } + + } + + } + } + // t.ContinuationToken (string) (string) + case "continuationToken": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.ContinuationToken = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *Bucket) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{164}); err != nil { + return err + } + + // t.ARN (string) (string) + if len("arn") > 8192 { + return xerrors.Errorf("Value in field \"arn\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("arn"))); err != nil { + return err + } + if _, err := cw.WriteString(string("arn")); err != nil { + return err + } + + if len(t.ARN) > 8192 { + return xerrors.Errorf("Value in field t.ARN was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.ARN))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.ARN)); err != nil { + return err + } + + // t.Name (string) (string) + if len("name") > 8192 { + return xerrors.Errorf("Value in field \"name\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil { + return err + } + if _, err := cw.WriteString(string("name")); err != nil { + return err + } + + if len(t.Name) > 8192 { + return xerrors.Errorf("Value in field t.Name was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Name)); err != nil { + return err + } + + // t.Region (string) (string) + if len("region") > 8192 { + return xerrors.Errorf("Value in field \"region\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("region"))); err != nil { + return err + } + if _, err := cw.WriteString(string("region")); err != nil { + return err + } + + if len(t.Region) > 8192 { + return xerrors.Errorf("Value in field t.Region was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Region))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Region)); err != nil { + return err + } + + // t.CreationDate (string) (string) + if len("creationDate") > 8192 { + return xerrors.Errorf("Value in field \"creationDate\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("creationDate"))); err != nil { + return err + } + if _, err := cw.WriteString(string("creationDate")); err != nil { + return err + } + + if len(t.CreationDate) > 8192 { + return xerrors.Errorf("Value in field t.CreationDate was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreationDate))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreationDate)); err != nil { + return err + } + return nil +} + +func (t *Bucket) UnmarshalCBOR(r io.Reader) (err error) { + *t = Bucket{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Bucket: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 12) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.ARN (string) (string) + case "arn": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.ARN = string(sval) + } + // t.Name (string) (string) + case "name": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Name = string(sval) + } + // t.Region (string) (string) + case "region": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Region = string(sval) + } + // t.CreationDate (string) (string) + case "creationDate": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.CreationDate = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *Owner) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.ID (string) (string) + if len("id") > 8192 { + return xerrors.Errorf("Value in field \"id\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("id"))); err != nil { + return err + } + if _, err := cw.WriteString(string("id")); err != nil { + return err + } + + if len(t.ID) > 8192 { + return xerrors.Errorf("Value in field t.ID was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.ID))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.ID)); err != nil { + return err + } + + // t.DisplayName (string) (string) + if len("displayName") > 8192 { + return xerrors.Errorf("Value in field \"displayName\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("displayName"))); err != nil { + return err + } + if _, err := cw.WriteString(string("displayName")); err != nil { + return err + } + + if len(t.DisplayName) > 8192 { + return xerrors.Errorf("Value in field t.DisplayName was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.DisplayName))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.DisplayName)); err != nil { + return err + } + return nil +} + +func (t *Owner) UnmarshalCBOR(r io.Reader) (err error) { + *t = Owner{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Owner: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.ID (string) (string) + case "id": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.ID = string(sval) + } + // t.DisplayName (string) (string) + case "displayName": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.DisplayName = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/s3/bucket/create.go b/protocol/commands/s3/bucket/create.go new file mode 100644 index 00000000..2045c96d --- /dev/null +++ b/protocol/commands/s3/bucket/create.go @@ -0,0 +1,15 @@ +//go:build !codegen + +package bucket + +import ( + "github.com/fil-forge/forge/protocol/commands/s3/request" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// Create is the `/s3/bucket/create` command. Ingot invokes it on Hilt to +// create a bucket and provision it with Sprue. It returns the same structure +// as `/s3/request/authorize` ([request.AuthorizeOK]): the new bucket DID and +// any delegations that now have access to it. +var Create = binding.Bind[*CreateArguments, *request.AuthorizeOK](command.MustParse("/s3/bucket/create")) diff --git a/protocol/commands/s3/bucket/delete.go b/protocol/commands/s3/bucket/delete.go new file mode 100644 index 00000000..714bd67d --- /dev/null +++ b/protocol/commands/s3/bucket/delete.go @@ -0,0 +1,17 @@ +//go:build !codegen + +package bucket + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// DeleteOK is the (empty) result of a successful `/s3/bucket/delete`. +type DeleteOK = commands.Unit + +// Delete is the `/s3/bucket/delete` command. Ingot invokes it on Hilt to +// delete an empty bucket, removing it from Hilt's stores and revoking any +// delegations that grant access to it. +var Delete = binding.Bind[*DeleteArguments, *DeleteOK](command.MustParse("/s3/bucket/delete")) diff --git a/hilt/pkg/rpc/service/bucket/errors.go b/protocol/commands/s3/bucket/errors.go similarity index 99% rename from hilt/pkg/rpc/service/bucket/errors.go rename to protocol/commands/s3/bucket/errors.go index 5d7aaa93..86dac69b 100644 --- a/hilt/pkg/rpc/service/bucket/errors.go +++ b/protocol/commands/s3/bucket/errors.go @@ -1,3 +1,5 @@ +//go:build !codegen + package bucket import "github.com/fil-forge/ucantone/errors" diff --git a/protocol/commands/s3/bucket/gen/main.go b/protocol/commands/s3/bucket/gen/main.go new file mode 100644 index 00000000..7be53197 --- /dev/null +++ b/protocol/commands/s3/bucket/gen/main.go @@ -0,0 +1,48 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/s3/bucket" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + bucket.CreateArguments{}, + bucket.DeleteArguments{}, + bucket.ListArguments{}, + bucket.InfoArguments{}, + bucket.InfoOK{}, + bucket.ListOK{}, + bucket.Bucket{}, + bucket.Owner{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "bucket", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "bucket", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/s3/bucket/info.go b/protocol/commands/s3/bucket/info.go new file mode 100644 index 00000000..dabaa5d1 --- /dev/null +++ b/protocol/commands/s3/bucket/info.go @@ -0,0 +1,13 @@ +//go:build !codegen + +package bucket + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// Info is the `/s3/bucket/info` command. Ingot invokes it on Hilt to look up a +// bucket by global name, returning the bucket DID and the delegation chain +// from the bucket to the given access key. +var Info = binding.Bind[*InfoArguments, *InfoOK](command.MustParse("/s3/bucket/info")) diff --git a/protocol/commands/s3/bucket/json_gen.go b/protocol/commands/s3/bucket/json_gen.go new file mode 100644 index 00000000..16d1db1c --- /dev/null +++ b/protocol/commands/s3/bucket/json_gen.go @@ -0,0 +1,1155 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package bucket + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *CreateArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Request (s3.Request) (struct) + if len("request") > 8192 { + return fmt.Errorf("string in field \"request\" was too long") + } + if err := jw.WriteString(string("request")); err != nil { + return fmt.Errorf("writing string for field \"request\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Request.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Request: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *CreateArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = CreateArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for CreateArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for CreateArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for CreateArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field CreateArguments: string too large") + } + return fmt.Errorf("reading string for field CreateArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field CreateArguments: %w", err) + } + switch name { + + // t.Request (s3.Request) (struct) + case "request": + + if err := t.Request.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Request: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for CreateArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field CreateArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for CreateArguments") + } + } + } + + return nil +} +func (t *DeleteArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Request (s3.Request) (struct) + if len("request") > 8192 { + return fmt.Errorf("string in field \"request\" was too long") + } + if err := jw.WriteString(string("request")); err != nil { + return fmt.Errorf("writing string for field \"request\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Request.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Request: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *DeleteArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = DeleteArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for DeleteArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for DeleteArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for DeleteArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field DeleteArguments: string too large") + } + return fmt.Errorf("reading string for field DeleteArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field DeleteArguments: %w", err) + } + switch name { + + // t.Request (s3.Request) (struct) + case "request": + + if err := t.Request.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Request: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for DeleteArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field DeleteArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for DeleteArguments") + } + } + } + + return nil +} +func (t *ListArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Request (s3.Request) (struct) + if len("request") > 8192 { + return fmt.Errorf("string in field \"request\" was too long") + } + if err := jw.WriteString(string("request")); err != nil { + return fmt.Errorf("writing string for field \"request\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Request.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Request: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListArguments: string too large") + } + return fmt.Errorf("reading string for field ListArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListArguments: %w", err) + } + switch name { + + // t.Request (s3.Request) (struct) + case "request": + + if err := t.Request.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Request: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListArguments") + } + } + } + + return nil +} +func (t *InfoArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.AccessKey (did.DID) (struct) + if len("accessKey") > 8192 { + return fmt.Errorf("string in field \"accessKey\" was too long") + } + if err := jw.WriteString(string("accessKey")); err != nil { + return fmt.Errorf("writing string for field \"accessKey\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.AccessKey.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.AccessKey: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Name (string) (string) + if len("name") > 8192 { + return fmt.Errorf("string in field \"name\" was too long") + } + if err := jw.WriteString(string("name")); err != nil { + return fmt.Errorf("writing string for field \"name\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Name) > 8192 { + return fmt.Errorf("string in field t.Name was too long") + } + if err := jw.WriteString(string(t.Name)); err != nil { + return fmt.Errorf("writing string for field t.Name: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *InfoArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = InfoArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for InfoArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for InfoArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for InfoArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field InfoArguments: string too large") + } + return fmt.Errorf("reading string for field InfoArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field InfoArguments: %w", err) + } + switch name { + + // t.AccessKey (did.DID) (struct) + case "accessKey": + + if err := t.AccessKey.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.AccessKey: %w", err) + } + + // t.Name (string) (string) + case "name": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.Name: string too long") + } + return fmt.Errorf("reading string for field t.Name: %w", err) + } + t.Name = string(sval) + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for InfoArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field InfoArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for InfoArguments") + } + } + } + + return nil +} +func (t *InfoOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Delegations (s3.ProofSet) (struct) + if len("delegations") > 8192 { + return fmt.Errorf("string in field \"delegations\" was too long") + } + if err := jw.WriteString(string("delegations")); err != nil { + return fmt.Errorf("writing string for field \"delegations\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Delegations.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Delegations: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.ID (did.DID) (struct) + if len("id") > 8192 { + return fmt.Errorf("string in field \"id\" was too long") + } + if err := jw.WriteString(string("id")); err != nil { + return fmt.Errorf("writing string for field \"id\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.ID.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.ID: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Permissions (s3.PermissionSet) (struct) + if len("permissions") > 8192 { + return fmt.Errorf("string in field \"permissions\" was too long") + } + if err := jw.WriteString(string("permissions")); err != nil { + return fmt.Errorf("writing string for field \"permissions\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Permissions.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Permissions: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *InfoOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = InfoOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for InfoOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for InfoOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for InfoOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field InfoOK: string too large") + } + return fmt.Errorf("reading string for field InfoOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field InfoOK: %w", err) + } + switch name { + + // t.Delegations (s3.ProofSet) (struct) + case "delegations": + + if err := t.Delegations.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Delegations: %w", err) + } + + // t.ID (did.DID) (struct) + case "id": + + if err := t.ID.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.ID: %w", err) + } + + // t.Permissions (s3.PermissionSet) (struct) + case "permissions": + + if err := t.Permissions.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Permissions: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for InfoOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field InfoOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for InfoOK") + } + } + } + + return nil +} +func (t *ListOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Buckets ([]bucket.Bucket) (slice) + if len("buckets") > 8192 { + return fmt.Errorf("string in field \"buckets\" was too long") + } + if err := jw.WriteString(string("buckets")); err != nil { + return fmt.Errorf("writing string for field \"buckets\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Buckets) > 8192 { + return fmt.Errorf("slice value in field t.Buckets was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Buckets: %w", err) + } + for i, v := range t.Buckets { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Buckets: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Buckets: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.ContinuationToken (string) (string) + if len("continuationToken") > 8192 { + return fmt.Errorf("string in field \"continuationToken\" was too long") + } + if err := jw.WriteString(string("continuationToken")); err != nil { + return fmt.Errorf("writing string for field \"continuationToken\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.ContinuationToken) > 8192 { + return fmt.Errorf("string in field t.ContinuationToken was too long") + } + if err := jw.WriteString(string(t.ContinuationToken)); err != nil { + return fmt.Errorf("writing string for field t.ContinuationToken: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Owner (bucket.Owner) (struct) + if len("owner") > 8192 { + return fmt.Errorf("string in field \"owner\" was too long") + } + if err := jw.WriteString(string("owner")); err != nil { + return fmt.Errorf("writing string for field \"owner\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Owner.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Owner: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Prefix (string) (string) + if len("prefix") > 8192 { + return fmt.Errorf("string in field \"prefix\" was too long") + } + if err := jw.WriteString(string("prefix")); err != nil { + return fmt.Errorf("writing string for field \"prefix\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Prefix) > 8192 { + return fmt.Errorf("string in field t.Prefix was too long") + } + if err := jw.WriteString(string(t.Prefix)); err != nil { + return fmt.Errorf("writing string for field t.Prefix: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListOK: string too large") + } + return fmt.Errorf("reading string for field ListOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListOK: %w", err) + } + switch name { + + // t.Buckets ([]bucket.Bucket) (slice) + case "buckets": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Buckets: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Buckets: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Buckets: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]Bucket, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Buckets = append(t.Buckets, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Buckets: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Buckets: slice too large") + } + } + } + + } + + // t.ContinuationToken (string) (string) + case "continuationToken": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.ContinuationToken: string too long") + } + return fmt.Errorf("reading string for field t.ContinuationToken: %w", err) + } + t.ContinuationToken = string(sval) + } + + // t.Owner (bucket.Owner) (struct) + case "owner": + + if err := t.Owner.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Owner: %w", err) + } + + // t.Prefix (string) (string) + case "prefix": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.Prefix: string too long") + } + return fmt.Errorf("reading string for field t.Prefix: %w", err) + } + t.Prefix = string(sval) + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListOK") + } + } + } + + return nil +} +func (t *Bucket) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.ARN (string) (string) + if len("arn") > 8192 { + return fmt.Errorf("string in field \"arn\" was too long") + } + if err := jw.WriteString(string("arn")); err != nil { + return fmt.Errorf("writing string for field \"arn\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.ARN) > 8192 { + return fmt.Errorf("string in field t.ARN was too long") + } + if err := jw.WriteString(string(t.ARN)); err != nil { + return fmt.Errorf("writing string for field t.ARN: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.CreationDate (string) (string) + if len("creationDate") > 8192 { + return fmt.Errorf("string in field \"creationDate\" was too long") + } + if err := jw.WriteString(string("creationDate")); err != nil { + return fmt.Errorf("writing string for field \"creationDate\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.CreationDate) > 8192 { + return fmt.Errorf("string in field t.CreationDate was too long") + } + if err := jw.WriteString(string(t.CreationDate)); err != nil { + return fmt.Errorf("writing string for field t.CreationDate: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Name (string) (string) + if len("name") > 8192 { + return fmt.Errorf("string in field \"name\" was too long") + } + if err := jw.WriteString(string("name")); err != nil { + return fmt.Errorf("writing string for field \"name\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Name) > 8192 { + return fmt.Errorf("string in field t.Name was too long") + } + if err := jw.WriteString(string(t.Name)); err != nil { + return fmt.Errorf("writing string for field t.Name: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Region (string) (string) + if len("region") > 8192 { + return fmt.Errorf("string in field \"region\" was too long") + } + if err := jw.WriteString(string("region")); err != nil { + return fmt.Errorf("writing string for field \"region\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Region) > 8192 { + return fmt.Errorf("string in field t.Region was too long") + } + if err := jw.WriteString(string(t.Region)); err != nil { + return fmt.Errorf("writing string for field t.Region: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *Bucket) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Bucket{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for Bucket: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for Bucket: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for Bucket: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field Bucket: string too large") + } + return fmt.Errorf("reading string for field Bucket: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field Bucket: %w", err) + } + switch name { + + // t.ARN (string) (string) + case "arn": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.ARN: string too long") + } + return fmt.Errorf("reading string for field t.ARN: %w", err) + } + t.ARN = string(sval) + } + + // t.CreationDate (string) (string) + case "creationDate": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.CreationDate: string too long") + } + return fmt.Errorf("reading string for field t.CreationDate: %w", err) + } + t.CreationDate = string(sval) + } + + // t.Name (string) (string) + case "name": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.Name: string too long") + } + return fmt.Errorf("reading string for field t.Name: %w", err) + } + t.Name = string(sval) + } + + // t.Region (string) (string) + case "region": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.Region: string too long") + } + return fmt.Errorf("reading string for field t.Region: %w", err) + } + t.Region = string(sval) + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for Bucket: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field Bucket: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for Bucket") + } + } + } + + return nil +} +func (t *Owner) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.DisplayName (string) (string) + if len("displayName") > 8192 { + return fmt.Errorf("string in field \"displayName\" was too long") + } + if err := jw.WriteString(string("displayName")); err != nil { + return fmt.Errorf("writing string for field \"displayName\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.DisplayName) > 8192 { + return fmt.Errorf("string in field t.DisplayName was too long") + } + if err := jw.WriteString(string(t.DisplayName)); err != nil { + return fmt.Errorf("writing string for field t.DisplayName: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.ID (string) (string) + if len("id") > 8192 { + return fmt.Errorf("string in field \"id\" was too long") + } + if err := jw.WriteString(string("id")); err != nil { + return fmt.Errorf("writing string for field \"id\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.ID) > 8192 { + return fmt.Errorf("string in field t.ID was too long") + } + if err := jw.WriteString(string(t.ID)); err != nil { + return fmt.Errorf("writing string for field t.ID: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *Owner) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Owner{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for Owner: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for Owner: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for Owner: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field Owner: string too large") + } + return fmt.Errorf("reading string for field Owner: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field Owner: %w", err) + } + switch name { + + // t.DisplayName (string) (string) + case "displayName": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.DisplayName: string too long") + } + return fmt.Errorf("reading string for field t.DisplayName: %w", err) + } + t.DisplayName = string(sval) + } + + // t.ID (string) (string) + case "id": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.ID: string too long") + } + return fmt.Errorf("reading string for field t.ID: %w", err) + } + t.ID = string(sval) + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for Owner: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field Owner: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for Owner") + } + } + } + + return nil +} diff --git a/protocol/commands/s3/bucket/list.go b/protocol/commands/s3/bucket/list.go new file mode 100644 index 00000000..8bfc6680 --- /dev/null +++ b/protocol/commands/s3/bucket/list.go @@ -0,0 +1,12 @@ +//go:build !codegen + +package bucket + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// List is the `/s3/bucket/list` command. Ingot invokes it on Hilt to list the +// buckets belonging to the tenant. +var List = binding.Bind[*ListArguments, *ListOK](command.MustParse("/s3/bucket/list")) diff --git a/protocol/commands/s3/bucket/types.go b/protocol/commands/s3/bucket/types.go new file mode 100644 index 00000000..46fe067f --- /dev/null +++ b/protocol/commands/s3/bucket/types.go @@ -0,0 +1,69 @@ +package bucket + +import ( + "github.com/fil-forge/forge/protocol/commands/s3" + "github.com/fil-forge/ucantone/did" +) + +// CreateArguments are the arguments to `/s3/bucket/create`. +type CreateArguments struct { + // Request is the AWS S3 CreateBucket request. + Request s3.Request `cborgen:"request" dagjsongen:"request"` +} + +// DeleteArguments are the arguments to `/s3/bucket/delete`. +type DeleteArguments struct { + // Request is the AWS S3 DeleteBucket request. + Request s3.Request `cborgen:"request" dagjsongen:"request"` +} + +// ListArguments are the arguments to `/s3/bucket/list`. +type ListArguments struct { + // Request is the AWS S3 ListBuckets request. + Request s3.Request `cborgen:"request" dagjsongen:"request"` +} + +// InfoArguments are the arguments to `/s3/bucket/info`. +type InfoArguments struct { + // Name is the global bucket name to look up. + Name string `cborgen:"name" dagjsongen:"name"` + // AccessKey is the access key DID the returned delegation chain should + // terminate at. + AccessKey did.DID `cborgen:"accessKey" dagjsongen:"accessKey"` +} + +// InfoOK is the successful result of `/s3/bucket/info`. Its Permissions and +// Delegations fields are slice-valued maps wrapped in struct types +// ([s3.PermissionSet] etc.) with their own codecs, so this struct is generated +// normally (the generators delegate to a struct field's codec). +type InfoOK struct { + // ID is the DID of the bucket. + ID did.DID `cborgen:"id" dagjsongen:"id"` + // Permissions maps the access key DID to its assigned S3 permissions. + Permissions s3.PermissionSet `cborgen:"permissions" dagjsongen:"permissions"` + // Delegations maps the CID of a delegation whose audience is the access key + // to its proof chain from the bucket. + Delegations s3.ProofSet `cborgen:"delegations" dagjsongen:"delegations"` +} + +// ListOK is the successful result of `/s3/bucket/list`. +type ListOK struct { + Buckets []Bucket `cborgen:"buckets" dagjsongen:"buckets"` + ContinuationToken string `cborgen:"continuationToken" dagjsongen:"continuationToken"` + Owner Owner `cborgen:"owner" dagjsongen:"owner"` + Prefix string `cborgen:"prefix" dagjsongen:"prefix"` +} + +// Bucket describes a single bucket in a `/s3/bucket/list` result. +type Bucket struct { + ARN string `cborgen:"arn" dagjsongen:"arn"` + Region string `cborgen:"region" dagjsongen:"region"` + CreationDate string `cborgen:"creationDate" dagjsongen:"creationDate"` + Name string `cborgen:"name" dagjsongen:"name"` +} + +// Owner is the owner of a set of buckets. +type Owner struct { + DisplayName string `cborgen:"displayName" dagjsongen:"displayName"` + ID string `cborgen:"id" dagjsongen:"id"` +} diff --git a/protocol/commands/s3/cbor_gen.go b/protocol/commands/s3/cbor_gen.go new file mode 100644 index 00000000..926e2318 --- /dev/null +++ b/protocol/commands/s3/cbor_gen.go @@ -0,0 +1,400 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package s3 + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *Request) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.URL (string) (string) + if len("url") > 8192 { + return xerrors.Errorf("Value in field \"url\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("url"))); err != nil { + return err + } + if _, err := cw.WriteString(string("url")); err != nil { + return err + } + + if len(t.URL) > 8192 { + return xerrors.Errorf("Value in field t.URL was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.URL))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.URL)); err != nil { + return err + } + + // t.Method (string) (string) + if len("method") > 8192 { + return xerrors.Errorf("Value in field \"method\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("method"))); err != nil { + return err + } + if _, err := cw.WriteString(string("method")); err != nil { + return err + } + + if len(t.Method) > 8192 { + return xerrors.Errorf("Value in field t.Method was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Method))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Method)); err != nil { + return err + } + + // t.Headers (map[string]string) (map) + if len("headers") > 8192 { + return xerrors.Errorf("Value in field \"headers\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("headers"))); err != nil { + return err + } + if _, err := cw.WriteString(string("headers")); err != nil { + return err + } + + { + if len(t.Headers) > 4096 { + return xerrors.Errorf("cannot marshal t.Headers map too large") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajMap, uint64(len(t.Headers))); err != nil { + return err + } + + keys := make([]string, 0, len(t.Headers)) + for k := range t.Headers { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := t.Headers[k] + + if len(k) > 8192 { + return xerrors.Errorf("Value in field k was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(k))); err != nil { + return err + } + if _, err := cw.WriteString(string(k)); err != nil { + return err + } + + if len(v) > 8192 { + return xerrors.Errorf("Value in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { + return err + } + if _, err := cw.WriteString(string(v)); err != nil { + return err + } + + } + } + return nil +} + +func (t *Request) UnmarshalCBOR(r io.Reader) (err error) { + *t = Request{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("Request: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.URL (string) (string) + case "url": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.URL = string(sval) + } + // t.Method (string) (string) + case "method": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Method = string(sval) + } + // t.Headers (map[string]string) (map) + case "headers": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajMap { + return fmt.Errorf("expected a map (major type 5)") + } + if extra > 4096 { + return fmt.Errorf("t.Headers: map too large") + } + + t.Headers = make(map[string]string, extra) + + for i, l := 0, int(extra); i < l; i++ { + + var k string + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + k = string(sval) + } + + var v string + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + v = string(sval) + } + + t.Headers[k] = v + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *VerificationKey) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Data ([]uint8) (slice) + if len("data") > 8192 { + return xerrors.Errorf("Value in field \"data\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("data"))); err != nil { + return err + } + if _, err := cw.WriteString(string("data")); err != nil { + return err + } + + if len(t.Data) > 2097152 { + return xerrors.Errorf("Byte array in field t.Data was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Data))); err != nil { + return err + } + + if _, err := cw.Write(t.Data); err != nil { + return err + } + + // t.Kind (string) (string) + if len("kind") > 8192 { + return xerrors.Errorf("Value in field \"kind\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("kind"))); err != nil { + return err + } + if _, err := cw.WriteString(string("kind")); err != nil { + return err + } + + if len(t.Kind) > 8192 { + return xerrors.Errorf("Value in field t.Kind was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Kind))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Kind)); err != nil { + return err + } + return nil +} + +func (t *VerificationKey) UnmarshalCBOR(r io.Reader) (err error) { + *t = VerificationKey{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("VerificationKey: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Data ([]uint8) (slice) + case "data": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Data: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Data = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Data); err != nil { + return err + } + + // t.Kind (string) (string) + case "kind": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Kind = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/s3/codec.go b/protocol/commands/s3/codec.go new file mode 100644 index 00000000..5e32c17b --- /dev/null +++ b/protocol/commands/s3/codec.go @@ -0,0 +1,518 @@ +//go:build !codegen + +package s3 + +import ( + "fmt" + "io" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/ucantone/did" + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +// These codecs are hand-written because cbor-gen / dag-json-gen do not support +// slice-valued maps. + +const ( + maxString = 8192 + maxLen = 4096 +) + +// --- shared CBOR helpers --- + +func writeCborStringField(cw *cbg.CborWriter, s string) error { + if len(s) > maxString { + return xerrors.Errorf("string value was too long (%d)", len(s)) + } + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(s))); err != nil { + return err + } + _, err := cw.WriteString(s) + return err +} + +func readCborStringField(cr *cbg.CborReader) (string, error) { + return cbg.ReadStringWithMax(cr, maxString) +} + +func writeCborArrayHeader(cw *cbg.CborWriter, n int) error { + if n > maxString { + return xerrors.Errorf("slice value was too long (%d)", n) + } + return cw.WriteMajorTypeHeader(cbg.MajArray, uint64(n)) +} + +func readCborArrayHeader(cr *cbg.CborReader) (uint64, error) { + maj, extra, err := cr.ReadHeader() + if err != nil { + return 0, err + } + if maj != cbg.MajArray { + return 0, fmt.Errorf("expected cbor array") + } + if extra > maxString { + return 0, fmt.Errorf("array too large (%d)", extra) + } + return extra, nil +} + +func writeCborMapHeader(cw *cbg.CborWriter, n int) error { + if n > maxLen { + return xerrors.Errorf("map too large (%d)", n) + } + return cw.WriteMajorTypeHeader(cbg.MajMap, uint64(n)) +} + +func readCborMapHeader(cr *cbg.CborReader) (uint64, error) { + maj, extra, err := cr.ReadHeader() + if err != nil { + return 0, err + } + if maj != cbg.MajMap { + return 0, fmt.Errorf("expected a map (major type 5)") + } + if extra > maxLen { + return 0, fmt.Errorf("map too large (%d)", extra) + } + return extra, nil +} + +// sortedDIDs returns the DID keys of m sorted by their string encoding, so the +// encoded map keys come out in the same (lexicographic) order the generators +// use for string-keyed maps. +func sortedDIDs[V any](m map[did.DID]V) []did.DID { + keys := make([]did.DID, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() }) + return keys +} + +// sortedCIDs returns the CID keys of m sorted by their string encoding. +func sortedCIDs[V any](m map[cid.Cid]V) []cid.Cid { + keys := make([]cid.Cid, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() }) + return keys +} + +// --- PermissionSet: map[did.DID][]string --- + +func (t PermissionSet) MarshalCBOR(w io.Writer) error { + cw := cbg.NewCborWriter(w) + if err := writeCborMapHeader(cw, len(t.Entries)); err != nil { + return err + } + for _, k := range sortedDIDs(t.Entries) { + if err := writeCborStringField(cw, k.String()); err != nil { + return err + } + perms := t.Entries[k] + if err := writeCborArrayHeader(cw, len(perms)); err != nil { + return err + } + for _, p := range perms { + if err := writeCborStringField(cw, p); err != nil { + return err + } + } + } + return nil +} + +func (t *PermissionSet) UnmarshalCBOR(r io.Reader) error { + cr := cbg.NewCborReader(r) + n, err := readCborMapHeader(cr) + if err != nil { + return err + } + m := make(map[did.DID][]string, n) + for i := uint64(0); i < n; i++ { + ks, err := readCborStringField(cr) + if err != nil { + return err + } + k, err := did.Parse(ks) + if err != nil { + return xerrors.Errorf("parsing access key did %q: %w", ks, err) + } + alen, err := readCborArrayHeader(cr) + if err != nil { + return err + } + perms := make([]string, 0, alen) + for j := uint64(0); j < alen; j++ { + p, err := readCborStringField(cr) + if err != nil { + return err + } + perms = append(perms, p) + } + m[k] = perms + } + *t = PermissionSet{Entries: m} + return nil +} + +func (t PermissionSet) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if err := jw.WriteObjectOpen(); err != nil { + return err + } + for i, k := range sortedDIDs(t.Entries) { + if err := writeJSONKey(jw, k.String(), i > 0); err != nil { + return err + } + if err := jw.WriteArrayOpen(); err != nil { + return err + } + for j, p := range t.Entries[k] { + if j > 0 { + if err := jw.WriteComma(); err != nil { + return err + } + } + if err := jw.WriteString(p); err != nil { + return err + } + } + if err := jw.WriteArrayClose(); err != nil { + return err + } + } + return jw.WriteObjectClose() +} + +func (t *PermissionSet) UnmarshalDagJSON(r io.Reader) error { + jr := jsg.NewDagJsonReader(r) + m := map[did.DID][]string{} + err := readJSONObject(jr, func(ks string) error { + k, err := did.Parse(ks) + if err != nil { + return xerrors.Errorf("parsing access key did %q: %w", ks, err) + } + perms, err := readJSONArray(jr, func() (string, error) { return jr.ReadString(maxString) }) + if err != nil { + return err + } + m[k] = perms + return nil + }) + if err != nil { + return err + } + *t = PermissionSet{Entries: m} + return nil +} + +// --- ProofSet: map[cid.Cid][]cid.Cid --- + +func (t ProofSet) MarshalCBOR(w io.Writer) error { + cw := cbg.NewCborWriter(w) + if err := writeCborMapHeader(cw, len(t.Entries)); err != nil { + return err + } + for _, k := range sortedCIDs(t.Entries) { + if err := writeCborStringField(cw, k.String()); err != nil { + return err + } + links := t.Entries[k] + if err := writeCborArrayHeader(cw, len(links)); err != nil { + return err + } + for _, c := range links { + if err := cbg.WriteCid(cw, c); err != nil { + return xerrors.Errorf("failed to write cid: %w", err) + } + } + } + return nil +} + +func (t *ProofSet) UnmarshalCBOR(r io.Reader) error { + cr := cbg.NewCborReader(r) + n, err := readCborMapHeader(cr) + if err != nil { + return err + } + m := make(map[cid.Cid][]cid.Cid, n) + for i := uint64(0); i < n; i++ { + ks, err := readCborStringField(cr) + if err != nil { + return err + } + k, err := cid.Decode(ks) + if err != nil { + return xerrors.Errorf("parsing delegation cid %q: %w", ks, err) + } + alen, err := readCborArrayHeader(cr) + if err != nil { + return err + } + links := make([]cid.Cid, 0, alen) + for j := uint64(0); j < alen; j++ { + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid: %w", err) + } + links = append(links, c) + } + m[k] = links + } + *t = ProofSet{Entries: m} + return nil +} + +func (t ProofSet) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if err := jw.WriteObjectOpen(); err != nil { + return err + } + for i, k := range sortedCIDs(t.Entries) { + if err := writeJSONKey(jw, k.String(), i > 0); err != nil { + return err + } + if err := jw.WriteArrayOpen(); err != nil { + return err + } + for j, c := range t.Entries[k] { + if j > 0 { + if err := jw.WriteComma(); err != nil { + return err + } + } + if err := jw.WriteCid(c); err != nil { + return err + } + } + if err := jw.WriteArrayClose(); err != nil { + return err + } + } + return jw.WriteObjectClose() +} + +func (t *ProofSet) UnmarshalDagJSON(r io.Reader) error { + jr := jsg.NewDagJsonReader(r) + m := map[cid.Cid][]cid.Cid{} + err := readJSONObject(jr, func(ks string) error { + k, err := cid.Decode(ks) + if err != nil { + return xerrors.Errorf("parsing delegation cid %q: %w", ks, err) + } + links, err := readJSONArray(jr, func() (cid.Cid, error) { return jr.ReadCid() }) + if err != nil { + return err + } + m[k] = links + return nil + }) + if err != nil { + return err + } + *t = ProofSet{Entries: m} + return nil +} + +// --- KeySet: map[did.DID][]VerificationKey --- + +func (t KeySet) MarshalCBOR(w io.Writer) error { + cw := cbg.NewCborWriter(w) + if err := writeCborMapHeader(cw, len(t.Entries)); err != nil { + return err + } + for _, k := range sortedDIDs(t.Entries) { + if err := writeCborStringField(cw, k.String()); err != nil { + return err + } + keys := t.Entries[k] + if err := writeCborArrayHeader(cw, len(keys)); err != nil { + return err + } + for i := range keys { + if err := keys[i].MarshalCBOR(cw); err != nil { + return err + } + } + } + return nil +} + +func (t *KeySet) UnmarshalCBOR(r io.Reader) error { + cr := cbg.NewCborReader(r) + n, err := readCborMapHeader(cr) + if err != nil { + return err + } + m := make(map[did.DID][]VerificationKey, n) + for i := uint64(0); i < n; i++ { + ks, err := readCborStringField(cr) + if err != nil { + return err + } + k, err := did.Parse(ks) + if err != nil { + return xerrors.Errorf("parsing access key did %q: %w", ks, err) + } + alen, err := readCborArrayHeader(cr) + if err != nil { + return err + } + keys := make([]VerificationKey, alen) + for j := uint64(0); j < alen; j++ { + if err := keys[j].UnmarshalCBOR(cr); err != nil { + return err + } + } + m[k] = keys + } + *t = KeySet{Entries: m} + return nil +} + +func (t KeySet) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if err := jw.WriteObjectOpen(); err != nil { + return err + } + for i, k := range sortedDIDs(t.Entries) { + if err := writeJSONKey(jw, k.String(), i > 0); err != nil { + return err + } + if err := jw.WriteArrayOpen(); err != nil { + return err + } + keys := t.Entries[k] + for j := range keys { + if j > 0 { + if err := jw.WriteComma(); err != nil { + return err + } + } + if err := keys[j].MarshalDagJSON(jw); err != nil { + return err + } + } + if err := jw.WriteArrayClose(); err != nil { + return err + } + } + return jw.WriteObjectClose() +} + +func (t *KeySet) UnmarshalDagJSON(r io.Reader) error { + jr := jsg.NewDagJsonReader(r) + m := map[did.DID][]VerificationKey{} + err := readJSONObject(jr, func(ks string) error { + k, err := did.Parse(ks) + if err != nil { + return xerrors.Errorf("parsing access key did %q: %w", ks, err) + } + keys, err := readJSONArray(jr, func() (VerificationKey, error) { + var vk VerificationKey + err := vk.UnmarshalDagJSON(jr) + return vk, err + }) + if err != nil { + return err + } + m[k] = keys + return nil + }) + if err != nil { + return err + } + *t = KeySet{Entries: m} + return nil +} + +// --- shared DAG-JSON helpers --- + +// writeJSONKey writes an object key, preceded by a comma when comma is true +// (i.e. when it is not the first entry of the object). +func writeJSONKey(jw *jsg.DagJsonWriter, name string, comma bool) error { + if comma { + if err := jw.WriteComma(); err != nil { + return err + } + } + if err := jw.WriteString(name); err != nil { + return err + } + return jw.WriteObjectColon() +} + +// readJSONObject reads a DAG-JSON object, invoking fn for each entry after +// consuming its key and colon. fn is responsible for reading the value. +func readJSONObject(jr *jsg.DagJsonReader, fn func(name string) error) (err error) { + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return err + } + close, err := jr.PeekObjectClose() + if err != nil { + return err + } + if close { + return jr.ReadObjectClose() + } + for { + name, err := jr.ReadString(maxString) + if err != nil { + return err + } + if err := jr.ReadObjectColon(); err != nil { + return err + } + if err := fn(name); err != nil { + return err + } + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return err + } + if close { + return nil + } + } +} + +// readJSONArray reads a DAG-JSON array, invoking read for each element. +func readJSONArray[T any](jr *jsg.DagJsonReader, read func() (T, error)) ([]T, error) { + if err := jr.ReadArrayOpen(); err != nil { + return nil, err + } + close, err := jr.PeekArrayClose() + if err != nil { + return nil, err + } + var out []T + if close { + return out, jr.ReadArrayClose() + } + for { + v, err := read() + if err != nil { + return nil, err + } + out = append(out, v) + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return nil, err + } + if close { + return out, nil + } + } +} diff --git a/protocol/commands/s3/codec_test.go b/protocol/commands/s3/codec_test.go new file mode 100644 index 00000000..69d0e6b6 --- /dev/null +++ b/protocol/commands/s3/codec_test.go @@ -0,0 +1,173 @@ +package s3_test + +import ( + "bytes" + "reflect" + "strings" + "testing" + + "github.com/fil-forge/forge/protocol/commands/s3" + "github.com/fil-forge/forge/protocol/commands/s3/bucket" + "github.com/fil-forge/forge/protocol/commands/s3/request" + "github.com/fil-forge/ucantone/did" + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" +) + +func mustCid(t *testing.T, s string) cid.Cid { + t.Helper() + c, err := cid.Parse(s) + if err != nil { + t.Fatalf("parsing cid %q: %v", s, err) + } + return c +} + +func ptr[T any](v T) *T { + return &v +} + +func TestRequestRoundTrip(t *testing.T) { + in := &s3.Request{ + Method: "GET", + Headers: map[string]string{ + "host": "region.s3.fil.one", + "x-amz-header": "a", + }, + URL: "https://region.s3.fil.one/bucket/path?x-id=GetObject", + } + + // CBOR + var cb bytes.Buffer + if err := in.MarshalCBOR(&cb); err != nil { + t.Fatalf("MarshalCBOR: %v", err) + } + var outCBOR s3.Request + if err := outCBOR.UnmarshalCBOR(bytes.NewReader(cb.Bytes())); err != nil { + t.Fatalf("UnmarshalCBOR: %v", err) + } + if !reflect.DeepEqual(*in, outCBOR) { + t.Fatalf("CBOR round-trip mismatch:\n got %#v\nwant %#v", outCBOR, *in) + } + + // DAG-JSON + var jb bytes.Buffer + if err := in.MarshalDagJSON(&jb); err != nil { + t.Fatalf("MarshalDagJSON: %v", err) + } + var outJSON s3.Request + if err := outJSON.UnmarshalDagJSON(bytes.NewReader(jb.Bytes())); err != nil { + t.Fatalf("UnmarshalDagJSON: %v\njson: %s", err, jb.String()) + } + if !reflect.DeepEqual(*in, outJSON) { + t.Fatalf("DAG-JSON round-trip mismatch:\n got %#v\nwant %#v", outJSON, *in) + } + // DAG-JSON keys are emitted in lexicographic order. + if got := jb.String(); !strings.HasPrefix(got, `{"headers":`) { + t.Fatalf("expected headers first in DAG-JSON, got: %s", got) + } +} + +func TestAuthorizeOKRoundTrip(t *testing.T) { + access := did.MustParse("did:key:z6MkjFRxLLGdBqQSLkZbVnuwUFiomK8eGBkPtim9ETvP7vec") + delCid := mustCid(t, "bafyreienos3cw7hcga5vwani3pberioe2qscnz5jk2um5jajo4v7bwmvvm") + + in := &request.AuthorizeOK{ + Bucket: ptr(did.MustParse("did:key:z6MkmNBgCewjYfEDTdKLpHkbMWUogJk29CxmiVdLeW4Kz3UG")), + Tenant: did.MustParse("did:plc:ewvi7nxzyoun6zhxrhs64oiz"), + Permissions: s3.PermissionSet{Entries: map[did.DID][]string{ + access: {"s3:GetObject", "s3:PutObject"}, + }}, + Keys: s3.KeySet{Entries: map[did.DID][]s3.VerificationKey{ + access: {{Kind: s3.KeyKindSigV4a, Data: []byte{1, 2, 3, 4}}}, + }}, + Delegations: s3.ProofSet{Entries: map[cid.Cid][]cid.Cid{ + delCid: {delCid}, + }}, + } + + var cb bytes.Buffer + if err := in.MarshalCBOR(&cb); err != nil { + t.Fatalf("MarshalCBOR: %v", err) + } + var outCBOR request.AuthorizeOK + if err := outCBOR.UnmarshalCBOR(bytes.NewReader(cb.Bytes())); err != nil { + t.Fatalf("UnmarshalCBOR: %v", err) + } + if !reflect.DeepEqual(*in, outCBOR) { + t.Fatalf("CBOR round-trip mismatch:\n got %#v\nwant %#v", outCBOR, *in) + } + + var jb bytes.Buffer + if err := in.MarshalDagJSON(&jb); err != nil { + t.Fatalf("MarshalDagJSON: %v", err) + } + var outJSON request.AuthorizeOK + if err := outJSON.UnmarshalDagJSON(bytes.NewReader(jb.Bytes())); err != nil { + t.Fatalf("UnmarshalDagJSON: %v\njson: %s", err, jb.String()) + } + if !reflect.DeepEqual(*in, outJSON) { + t.Fatalf("DAG-JSON round-trip mismatch:\n got %#v\nwant %#v", outJSON, *in) + } +} + +func TestAuthorizeOKEmptyValuesRoundTrip(t *testing.T) { + // Nil/empty slice-valued maps must encode as empty objects and decode back. + in := &request.AuthorizeOK{ + Permissions: s3.PermissionSet{}, + Keys: s3.KeySet{}, + Delegations: s3.ProofSet{}, + } + var jb bytes.Buffer + if err := in.MarshalDagJSON(&jb); err != nil { + t.Fatalf("MarshalDagJSON: %v", err) + } + var out request.AuthorizeOK + if err := out.UnmarshalDagJSON(bytes.NewReader(jb.Bytes())); err != nil { + t.Fatalf("UnmarshalDagJSON: %v\njson: %s", err, jb.String()) + } + require.Nil(t, out.Bucket) + if len(out.Permissions.Entries) != 0 || len(out.Keys.Entries) != 0 || len(out.Delegations.Entries) != 0 { + t.Fatalf("expected empty maps, got %#v", out) + } +} + +func TestInfoOKRoundTrip(t *testing.T) { + access := did.MustParse("did:key:z6MkjFRxLLGdBqQSLkZbVnuwUFiomK8eGBkPtim9ETvP7vec") + root := mustCid(t, "bafyreiabuvg5hkupzjfn2kqywbdp5xhsb25pmhviyfz77yxhspssvxsv5y") + inter := mustCid(t, "bafyreigngbemvzgbmelwddwoms2ak2g32vmhcpxg6dqlwvb6spiezoc4py") + + in := &bucket.InfoOK{ + ID: did.MustParse("did:key:z6MkmNBgCewjYfEDTdKLpHkbMWUogJk29CxmiVdLeW4Kz3UG"), + Permissions: s3.PermissionSet{Entries: map[did.DID][]string{ + access: {"s3:GetObject", "s3:PutObject"}, + }}, + Delegations: s3.ProofSet{Entries: map[cid.Cid][]cid.Cid{ + inter: {root, inter}, + }}, + } + + var cb bytes.Buffer + if err := in.MarshalCBOR(&cb); err != nil { + t.Fatalf("MarshalCBOR: %v", err) + } + var outCBOR bucket.InfoOK + if err := outCBOR.UnmarshalCBOR(bytes.NewReader(cb.Bytes())); err != nil { + t.Fatalf("UnmarshalCBOR: %v", err) + } + if !reflect.DeepEqual(*in, outCBOR) { + t.Fatalf("CBOR round-trip mismatch:\n got %#v\nwant %#v", outCBOR, *in) + } + + var jb bytes.Buffer + if err := in.MarshalDagJSON(&jb); err != nil { + t.Fatalf("MarshalDagJSON: %v", err) + } + var outJSON bucket.InfoOK + if err := outJSON.UnmarshalDagJSON(bytes.NewReader(jb.Bytes())); err != nil { + t.Fatalf("UnmarshalDagJSON: %v\njson: %s", err, jb.String()) + } + if !reflect.DeepEqual(*in, outJSON) { + t.Fatalf("DAG-JSON round-trip mismatch:\n got %#v\nwant %#v", outJSON, *in) + } +} diff --git a/protocol/commands/s3/gen/main.go b/protocol/commands/s3/gen/main.go new file mode 100644 index 00000000..6174fcea --- /dev/null +++ b/protocol/commands/s3/gen/main.go @@ -0,0 +1,45 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/s3" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + // PermissionSet, KeySet and ProofSet have hand-written codecs (see + // codec.go) because cbor-gen / dag-json-gen do not support slice-valued + // maps. + models := []any{ + s3.Request{}, + s3.VerificationKey{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "s3", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "s3", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/s3/json_gen.go b/protocol/commands/s3/json_gen.go new file mode 100644 index 00000000..1c563550 --- /dev/null +++ b/protocol/commands/s3/json_gen.go @@ -0,0 +1,413 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package s3 + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *Request) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Headers (map[string]string) (map) + if len("headers") > 8192 { + return fmt.Errorf("string in field \"headers\" was too long") + } + if err := jw.WriteString(string("headers")); err != nil { + return fmt.Errorf("writing string for field \"headers\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + { + if len(t.Headers) > 4096 { + return fmt.Errorf("cannot marshal t.Headers map too large") + } + + if err := jw.WriteObjectOpen(); err != nil { + return fmt.Errorf("writing object open for field t.Headers: %w", err) + } + + keys := make([]string, 0, len(t.Headers)) + for k := range t.Headers { + keys = append(keys, k) + } + sort.Strings(keys) + for i, k := range keys { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Headers: %w", err) + } + } + v := t.Headers[k] + if len(k) > 8192 { + return fmt.Errorf("string in field k was too long") + } + if err := jw.WriteString(string(k)); err != nil { + return fmt.Errorf("writing string for field k: %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return fmt.Errorf("writing colon for field t.Headers: %w", err) + } + + if len(v) > 8192 { + return fmt.Errorf("string in field v was too long") + } + if err := jw.WriteString(string(v)); err != nil { + return fmt.Errorf("writing string for field v: %w", err) + } + } + if err := jw.WriteObjectClose(); err != nil { + return fmt.Errorf("writing object close for field t.Headers: %w", err) + } + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Method (string) (string) + if len("method") > 8192 { + return fmt.Errorf("string in field \"method\" was too long") + } + if err := jw.WriteString(string("method")); err != nil { + return fmt.Errorf("writing string for field \"method\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Method) > 8192 { + return fmt.Errorf("string in field t.Method was too long") + } + if err := jw.WriteString(string(t.Method)); err != nil { + return fmt.Errorf("writing string for field t.Method: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.URL (string) (string) + if len("url") > 8192 { + return fmt.Errorf("string in field \"url\" was too long") + } + if err := jw.WriteString(string("url")); err != nil { + return fmt.Errorf("writing string for field \"url\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.URL) > 8192 { + return fmt.Errorf("string in field t.URL was too long") + } + if err := jw.WriteString(string(t.URL)); err != nil { + return fmt.Errorf("writing string for field t.URL: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *Request) UnmarshalDagJSON(r io.Reader) (err error) { + *t = Request{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for Request: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for Request: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for Request: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field Request: string too large") + } + return fmt.Errorf("reading string for field Request: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field Request: %w", err) + } + switch name { + + // t.Headers (map[string]string) (map) + case "headers": + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for field t.Headers: %w", err) + } + + t.Headers = map[string]string{} + + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for field t.Headers: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for field t.Headers: %w", err) + } + } else { + for i, l := 0, 8192; i < l; i++ { + var k string + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field k: string too long") + } + return fmt.Errorf("reading string for field k: %w", err) + } + k = string(sval) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field t.Headers: %w", err) + } + var v string + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field v: string too long") + } + return fmt.Errorf("reading string for field v: %w", err) + } + v = string(sval) + } + t.Headers[k] = v + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field t.Headers: %w", err) + } + if close { + break + } + } + } + + // t.Method (string) (string) + case "method": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.Method: string too long") + } + return fmt.Errorf("reading string for field t.Method: %w", err) + } + t.Method = string(sval) + } + + // t.URL (string) (string) + case "url": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.URL: string too long") + } + return fmt.Errorf("reading string for field t.URL: %w", err) + } + t.URL = string(sval) + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for Request: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field Request: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for Request") + } + } + } + + return nil +} +func (t *VerificationKey) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Data ([]uint8) (slice) + if len("data") > 8192 { + return fmt.Errorf("string in field \"data\" was too long") + } + if err := jw.WriteString(string("data")); err != nil { + return fmt.Errorf("writing string for field \"data\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Data) > 2097152 { + return fmt.Errorf("byte array in field t.Data was too long") + } + + if err := jw.WriteBytes(t.Data); err != nil { + return fmt.Errorf("writing bytes for field t.Data: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Kind (string) (string) + if len("kind") > 8192 { + return fmt.Errorf("string in field \"kind\" was too long") + } + if err := jw.WriteString(string("kind")); err != nil { + return fmt.Errorf("writing string for field \"kind\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Kind) > 8192 { + return fmt.Errorf("string in field t.Kind was too long") + } + if err := jw.WriteString(string(t.Kind)); err != nil { + return fmt.Errorf("writing string for field t.Kind: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *VerificationKey) UnmarshalDagJSON(r io.Reader) (err error) { + *t = VerificationKey{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for VerificationKey: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for VerificationKey: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for VerificationKey: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field VerificationKey: string too large") + } + return fmt.Errorf("reading string for field VerificationKey: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field VerificationKey: %w", err) + } + switch name { + + // t.Data ([]uint8) (slice) + case "data": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Data: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Data: %w", err) + } + if len(bval) > 0 { + t.Data = []uint8(bval) + } + } + + // t.Kind (string) (string) + case "kind": + { + sval, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field t.Kind: string too long") + } + return fmt.Errorf("reading string for field t.Kind: %w", err) + } + t.Kind = string(sval) + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for VerificationKey: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field VerificationKey: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for VerificationKey") + } + } + } + + return nil +} diff --git a/hilt/pkg/rpc/service/auth/operation.go b/protocol/commands/s3/operation.go similarity index 90% rename from hilt/pkg/rpc/service/auth/operation.go rename to protocol/commands/s3/operation.go index 21105a6e..69510f33 100644 --- a/hilt/pkg/rpc/service/auth/operation.go +++ b/protocol/commands/s3/operation.go @@ -1,18 +1,18 @@ -package auth +//go:build !codegen + +package s3 import ( "fmt" "net/http" "net/url" "strings" - - s3 "github.com/fil-forge/libforge/commands/s3" ) // Operation is the S3 operation a request performs, derived from its HTTP method -// and path. [Authorizer.Authorize] classifies it, checks the access key is -// permitted to perform it, and returns it on [AuthorizedRequest.Operation] so a -// handler can confirm the request matches the operation it serves. +// and path. Hilt's authorize service classifies it, checks the access key is +// permitted to perform it, and returns it to the caller so a handler can confirm +// the request matches the operation it serves. type Operation string const ( @@ -62,11 +62,11 @@ func (o Operation) Permission() string { return operationPermission[o] } func (o Operation) String() string { return string(o) } -// addressesExistingBucket reports whether the operation acts on a bucket that must +// AddressesExistingBucket reports whether the operation acts on a bucket that must // already exist, so it can be resolved and scope-checked. ListBuckets addresses no // bucket; CreateBucket's bucket does not exist yet. Every multipart operation acts // on an existing bucket — an upload cannot be initiated into one that does not. -func (o Operation) addressesExistingBucket() bool { +func (o Operation) AddressesExistingBucket() bool { switch o { case OpListBucket, OpGetObject, OpPutObject, OpDeleteObject, OpDeleteBucket, OpCreateMultipartUpload, OpUploadPart, OpCompleteMultipartUpload, @@ -78,13 +78,13 @@ func (o Operation) addressesExistingBucket() bool { } // OperationFor classifies the S3 operation addressed by a request. See -// [classifyRequest] for the method/path rules. -func OperationFor(req s3.Request) (Operation, error) { - op, _, _, err := classifyRequest(req) +// [ClassifyRequest] for the method/path rules. +func OperationFor(req Request) (Operation, error) { + op, _, _, err := ClassifyRequest(req) return op, err } -// classifyRequest determines the S3 operation and the addressed bucket/object key +// ClassifyRequest determines the S3 operation and the addressed bucket/object key // from a request's HTTP method, path-style URL (https:////) and // query parameters. Both the path and the query string are part of the SigV4-signed // canonical request (see sigv4.canonicalQueryString), so once the signature is @@ -103,7 +103,7 @@ func OperationFor(req s3.Request) (Operation, error) { // the target and s3:GetObject on the source object, but the source arrives in the // x-amz-copy-source header, which is not guaranteed to be signed. Only s3:PutObject // is required, matching how a plain CopyObject also classifies as OpPutObject. -func classifyRequest(req s3.Request) (op Operation, bucket, key string, err error) { +func ClassifyRequest(req Request) (op Operation, bucket, key string, err error) { u, err := url.Parse(req.URL) if err != nil { return "", "", "", fmt.Errorf("parsing request URL: %w", err) diff --git a/hilt/pkg/rpc/service/auth/operation_test.go b/protocol/commands/s3/operation_test.go similarity index 96% rename from hilt/pkg/rpc/service/auth/operation_test.go rename to protocol/commands/s3/operation_test.go index 9e253dac..da6fc46d 100644 --- a/hilt/pkg/rpc/service/auth/operation_test.go +++ b/protocol/commands/s3/operation_test.go @@ -1,9 +1,10 @@ -package auth +//go:build !codegen + +package s3 import ( "testing" - s3 "github.com/fil-forge/libforge/commands/s3" "github.com/stretchr/testify/require" ) @@ -63,7 +64,7 @@ func TestClassifyRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - op, bucket, key, err := classifyRequest(s3.Request{Method: tt.method, URL: tt.url}) + op, bucket, key, err := ClassifyRequest(Request{Method: tt.method, URL: tt.url}) require.NoError(t, err) require.Equal(t, tt.want, op) require.Equal(t, tt.wantBucket, bucket) @@ -72,13 +73,13 @@ func TestClassifyRequest(t *testing.T) { } t.Run("rejects unsupported method and path combinations", func(t *testing.T) { - for _, req := range []s3.Request{ + for _, req := range []Request{ {Method: "PUT", URL: "https://s3.example.com/"}, {Method: "POST", URL: "https://s3.example.com/"}, {Method: "DELETE", URL: "https://s3.example.com/"}, {Method: "PATCH", URL: "https://s3.example.com/bkt/k"}, } { - _, _, _, err := classifyRequest(req) + _, _, _, err := ClassifyRequest(req) require.Error(t, err, "%s %s", req.Method, req.URL) } }) diff --git a/protocol/commands/s3/request/authorize.go b/protocol/commands/s3/request/authorize.go new file mode 100644 index 00000000..f0ce0599 --- /dev/null +++ b/protocol/commands/s3/request/authorize.go @@ -0,0 +1,15 @@ +//go:build !codegen + +package request + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// Authorize is the `/s3/request/authorize` command. Ingot invokes it on Hilt +// (issuer = Ingot, audience = subject = Hilt) to authorize an AWS S3 API +// request: Hilt verifies the SigV4 signature, looks up the access key's +// delegations, derives a signing key and re-delegates capabilities to the +// invocation issuer. +var Authorize = binding.Bind[*AuthorizeArguments, *AuthorizeOK](command.MustParse("/s3/request/authorize")) diff --git a/protocol/commands/s3/request/cbor_gen.go b/protocol/commands/s3/request/cbor_gen.go new file mode 100644 index 00000000..4a6eab04 --- /dev/null +++ b/protocol/commands/s3/request/cbor_gen.go @@ -0,0 +1,329 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package request + +import ( + "fmt" + "io" + "math" + "sort" + + did "github.com/fil-forge/ucantone/did" + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *AuthorizeArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Request (s3.Request) (struct) + if len("request") > 8192 { + return xerrors.Errorf("Value in field \"request\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("request"))); err != nil { + return err + } + if _, err := cw.WriteString(string("request")); err != nil { + return err + } + + if err := t.Request.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AuthorizeArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AuthorizeArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AuthorizeArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Request (s3.Request) (struct) + case "request": + + { + + if err := t.Request.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Request: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *AuthorizeOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 5 + + if t.Bucket == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Keys (s3.KeySet) (struct) + if len("keys") > 8192 { + return xerrors.Errorf("Value in field \"keys\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("keys"))); err != nil { + return err + } + if _, err := cw.WriteString(string("keys")); err != nil { + return err + } + + if err := t.Keys.MarshalCBOR(cw); err != nil { + return err + } + + // t.Bucket (did.DID) (struct) + if t.Bucket != nil { + + if len("bucket") > 8192 { + return xerrors.Errorf("Value in field \"bucket\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("bucket"))); err != nil { + return err + } + if _, err := cw.WriteString(string("bucket")); err != nil { + return err + } + + if err := t.Bucket.MarshalCBOR(cw); err != nil { + return err + } + } + + // t.Tenant (did.DID) (struct) + if len("tenant") > 8192 { + return xerrors.Errorf("Value in field \"tenant\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("tenant"))); err != nil { + return err + } + if _, err := cw.WriteString(string("tenant")); err != nil { + return err + } + + if err := t.Tenant.MarshalCBOR(cw); err != nil { + return err + } + + // t.Delegations (s3.ProofSet) (struct) + if len("delegations") > 8192 { + return xerrors.Errorf("Value in field \"delegations\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("delegations"))); err != nil { + return err + } + if _, err := cw.WriteString(string("delegations")); err != nil { + return err + } + + if err := t.Delegations.MarshalCBOR(cw); err != nil { + return err + } + + // t.Permissions (s3.PermissionSet) (struct) + if len("permissions") > 8192 { + return xerrors.Errorf("Value in field \"permissions\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("permissions"))); err != nil { + return err + } + if _, err := cw.WriteString(string("permissions")); err != nil { + return err + } + + if err := t.Permissions.MarshalCBOR(cw); err != nil { + return err + } + return nil +} + +func (t *AuthorizeOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = AuthorizeOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AuthorizeOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Keys (s3.KeySet) (struct) + case "keys": + + { + + if err := t.Keys.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Keys: %w", err) + } + + } + // t.Bucket (did.DID) (struct) + case "bucket": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + t.Bucket = new(did.DID) + if err := t.Bucket.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Bucket pointer: %w", err) + } + } + + } + // t.Tenant (did.DID) (struct) + case "tenant": + + { + + if err := t.Tenant.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Tenant: %w", err) + } + + } + // t.Delegations (s3.ProofSet) (struct) + case "delegations": + + { + + if err := t.Delegations.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Delegations: %w", err) + } + + } + // t.Permissions (s3.PermissionSet) (struct) + case "permissions": + + { + + if err := t.Permissions.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Permissions: %w", err) + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/hilt/pkg/rpc/service/auth/errors.go b/protocol/commands/s3/request/errors.go similarity index 96% rename from hilt/pkg/rpc/service/auth/errors.go rename to protocol/commands/s3/request/errors.go index 6c7572b7..c2d6db3d 100644 --- a/hilt/pkg/rpc/service/auth/errors.go +++ b/protocol/commands/s3/request/errors.go @@ -1,4 +1,6 @@ -package auth +//go:build !codegen + +package request import "github.com/fil-forge/ucantone/errors" @@ -21,7 +23,8 @@ const ( BucketNotPermittedErrorName = "BucketNotPermitted" ) -// Named rejection errors returned by [Authorizer.Authorize]. Each is a sentinel +// Named rejection errors returned by Hilt's signature-authorized /s3/* commands +// (its Authorizer service). Each is a sentinel // carrying a stable Name(), wrapped with per-request context at the return site, // so callers can branch on the reason with errors.Is. Unexpected/internal // failures (store or vault errors) are intentionally not named — they are diff --git a/protocol/commands/s3/request/gen/main.go b/protocol/commands/s3/request/gen/main.go new file mode 100644 index 00000000..a08aa270 --- /dev/null +++ b/protocol/commands/s3/request/gen/main.go @@ -0,0 +1,42 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/s3/request" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + request.AuthorizeArguments{}, + request.AuthorizeOK{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "request", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "request", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/s3/request/json_gen.go b/protocol/commands/s3/request/json_gen.go new file mode 100644 index 00000000..33c36698 --- /dev/null +++ b/protocol/commands/s3/request/json_gen.go @@ -0,0 +1,329 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package request + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + did "github.com/fil-forge/ucantone/did" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *AuthorizeArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Request (s3.Request) (struct) + if len("request") > 8192 { + return fmt.Errorf("string in field \"request\" was too long") + } + if err := jw.WriteString(string("request")); err != nil { + return fmt.Errorf("writing string for field \"request\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Request.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Request: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AuthorizeArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AuthorizeArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AuthorizeArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AuthorizeArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AuthorizeArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AuthorizeArguments: string too large") + } + return fmt.Errorf("reading string for field AuthorizeArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AuthorizeArguments: %w", err) + } + switch name { + + // t.Request (s3.Request) (struct) + case "request": + + if err := t.Request.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Request: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AuthorizeArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AuthorizeArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AuthorizeArguments") + } + } + } + + return nil +} +func (t *AuthorizeOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Bucket (did.DID) (struct) + if t.Bucket != nil { + if len("bucket") > 8192 { + return fmt.Errorf("string in field \"bucket\" was too long") + } + if err := jw.WriteString(string("bucket")); err != nil { + return fmt.Errorf("writing string for field \"bucket\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Bucket.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Bucket: %w", err) + } + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Delegations (s3.ProofSet) (struct) + if len("delegations") > 8192 { + return fmt.Errorf("string in field \"delegations\" was too long") + } + if err := jw.WriteString(string("delegations")); err != nil { + return fmt.Errorf("writing string for field \"delegations\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Delegations.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Delegations: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Keys (s3.KeySet) (struct) + if len("keys") > 8192 { + return fmt.Errorf("string in field \"keys\" was too long") + } + if err := jw.WriteString(string("keys")); err != nil { + return fmt.Errorf("writing string for field \"keys\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Keys.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Keys: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Permissions (s3.PermissionSet) (struct) + if len("permissions") > 8192 { + return fmt.Errorf("string in field \"permissions\" was too long") + } + if err := jw.WriteString(string("permissions")); err != nil { + return fmt.Errorf("writing string for field \"permissions\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Permissions.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Permissions: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Tenant (did.DID) (struct) + if len("tenant") > 8192 { + return fmt.Errorf("string in field \"tenant\" was too long") + } + if err := jw.WriteString(string("tenant")); err != nil { + return fmt.Errorf("writing string for field \"tenant\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Tenant.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Tenant: %w", err) + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AuthorizeOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AuthorizeOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AuthorizeOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AuthorizeOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AuthorizeOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AuthorizeOK: string too large") + } + return fmt.Errorf("reading string for field AuthorizeOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AuthorizeOK: %w", err) + } + switch name { + + // t.Bucket (did.DID) (struct) + case "bucket": + + { + null, err := jr.PeekNull() + if err != nil { + return fmt.Errorf("peeking null for field t.Bucket: %w", err) + } + if null { + if err := jr.ReadNull(); err != nil { + return fmt.Errorf("reading null for field t.Bucket: %w", err) + } + } else { + t.Bucket = new(did.DID) + if err := t.Bucket.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Bucket pointer: %w", err) + } + } + } + + // t.Delegations (s3.ProofSet) (struct) + case "delegations": + + if err := t.Delegations.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Delegations: %w", err) + } + + // t.Keys (s3.KeySet) (struct) + case "keys": + + if err := t.Keys.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Keys: %w", err) + } + + // t.Permissions (s3.PermissionSet) (struct) + case "permissions": + + if err := t.Permissions.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Permissions: %w", err) + } + + // t.Tenant (did.DID) (struct) + case "tenant": + + if err := t.Tenant.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Tenant: %w", err) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AuthorizeOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AuthorizeOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AuthorizeOK") + } + } + } + + return nil +} diff --git a/protocol/commands/s3/request/types.go b/protocol/commands/s3/request/types.go new file mode 100644 index 00000000..e9901050 --- /dev/null +++ b/protocol/commands/s3/request/types.go @@ -0,0 +1,39 @@ +package request + +import ( + "github.com/fil-forge/forge/protocol/commands/s3" + "github.com/fil-forge/ucantone/did" +) + +// AuthorizeArguments are the arguments to the `/s3/request/authorize` command. +type AuthorizeArguments struct { + // Request is the AWS S3 API request to authorize. + Request s3.Request `cborgen:"request" dagjsongen:"request"` +} + +// AuthorizeOK is the successful result of `/s3/request/authorize`. It carries +// the resolved bucket DID, the tenant DID, the S3 permission set for the access +// key, the derived signing key(s) and the (24-hour TTL) delegations +// re-delegated to the invocation issuer. +// +// Its Permissions, Keys and Delegations fields are slice-valued maps that +// cbor-gen / dag-json-gen cannot generate inline, but they are wrapped in +// struct types ([s3.PermissionSet] etc.) with their own codecs, so this struct +// is generated normally (the generators delegate to a struct field's codec). +type AuthorizeOK struct { + // Bucket is the DID of the bucket addressed by the request. Note: not all + // requests are bucket-scoped, so this field may be nil. e.g. CreateBucket, + // ListAllMyBuckets, etc. + Bucket *did.DID `cborgen:"bucket,omitempty" dagjsongen:"bucket,omitempty"` + // Tenant is the DID of the tenant the access key belongs to (the tenant's + // did:plc). The gateway resolves its DID document to obtain the tenant's + // wrap key — the FEE tenant recipient every stored object is encrypted to. + Tenant did.DID `cborgen:"tenant" dagjsongen:"tenant"` + // Permissions maps the access key DID to its assigned S3 permissions. + Permissions s3.PermissionSet `cborgen:"permissions" dagjsongen:"permissions"` + // Keys maps the access key DID to its derived signing key(s). + Keys s3.KeySet `cborgen:"keys" dagjsongen:"keys"` + // Delegations maps the CID of a delegation whose audience is the invocation + // issuer to its proof chain. + Delegations s3.ProofSet `cborgen:"delegations" dagjsongen:"delegations"` +} diff --git a/protocol/commands/s3/types.go b/protocol/commands/s3/types.go new file mode 100644 index 00000000..36f17bdf --- /dev/null +++ b/protocol/commands/s3/types.go @@ -0,0 +1,69 @@ +// Package s3 defines the shared wire types for the Hilt S3 tenant-management +// UCAN API (the `/s3/...` commands). The individual commands live in the +// request and bucket subpackages; this package holds the types shared across +// them. +// +// PermissionSet, KeySet and ProofSet carry maps whose values are lists +// (e.g. map[did.DID][]string). The cbor-gen / dag-json-gen code generators do +// not support slice-valued maps, so these three types have hand-written codecs +// (see codec.go). They are defined as structs wrapping a map so that structs +// embedding them (AuthorizeOK, InfoOK) can still be generated as usual. +package s3 + +import ( + "github.com/fil-forge/ucantone/did" + "github.com/ipfs/go-cid" +) + +// KeyKind identifies the signing scheme of a derived [VerificationKey]. +const ( + KeyKindSigV4 = "sigv4" + KeyKindSigV4a = "sigv4a" +) + +// Request is an AWS S3 API request as forwarded by Ingot to Hilt. It carries +// enough of the original HTTP request (method, headers and URL) for Hilt to +// verify the SigV4/SigV4a signature and determine the requested S3 action. +type Request struct { + Method string `cborgen:"method" dagjsongen:"method"` + Headers map[string]string `cborgen:"headers" dagjsongen:"headers"` + URL string `cborgen:"url" dagjsongen:"url"` +} + +// VerificationKey is a SigV4/SigV4a derived signing key returned by Hilt so +// that Ingot can verify request signatures locally. Kind is one of +// [KeyKindSigV4] or [KeyKindSigV4a]. +type VerificationKey struct { + Kind string `cborgen:"kind" dagjsongen:"kind"` + Data []byte `cborgen:"data" dagjsongen:"data"` +} + +// PermissionSet maps an access key DID to the list of S3 permission strings +// (e.g. "s3:GetObject") granted to it. The DID is encoded as the (string) map +// key on the wire. +// +// It is a struct wrapping the map (rather than a named map type) so that +// structs embedding it can be generated by cbor-gen / dag-json-gen, which +// delegate to a struct field's MarshalCBOR but cannot generate a (slice-valued) +// map field inline. On the wire it encodes as the bare map, not as an object +// with an "entries" key. +type PermissionSet struct { + Entries map[did.DID][]string +} + +// KeySet maps an access key DID to its derived signing keys. Hilt returns the +// key kind matching the request signature and MAY include the other kind. The +// DID is encoded as the (string) map key on the wire. See [PermissionSet] for +// why this is a struct wrapping a map. +type KeySet struct { + Entries map[did.DID][]VerificationKey +} + +// ProofSet maps a delegation CID to a proof chain: the list of delegation CIDs +// that make up the chain. In the initial implementation a chain holds a single +// delegation, but it is modeled as a list to allow more in the future. The CID +// is encoded as the (string) map key on the wire; the chain entries are encoded +// as CID links. See [PermissionSet] for why this is a struct wrapping a map. +type ProofSet struct { + Entries map[cid.Cid][]cid.Cid +} diff --git a/protocol/commands/space/cbor_gen.go b/protocol/commands/space/cbor_gen.go new file mode 100644 index 00000000..c1793a2a --- /dev/null +++ b/protocol/commands/space/cbor_gen.go @@ -0,0 +1,154 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package space + +import ( + "fmt" + "io" + "math" + "sort" + + did "github.com/fil-forge/ucantone/did" + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *InfoOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Providers ([]did.DID) (slice) + if len("providers") > 8192 { + return xerrors.Errorf("Value in field \"providers\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("providers"))); err != nil { + return err + } + if _, err := cw.WriteString(string("providers")); err != nil { + return err + } + + if len(t.Providers) > 8192 { + return xerrors.Errorf("Slice value in field t.Providers was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Providers))); err != nil { + return err + } + for _, v := range t.Providers { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *InfoOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = InfoOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("InfoOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Providers ([]did.DID) (slice) + case "providers": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Providers: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Providers = make([]did.DID, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Providers[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Providers[i]: %w", err) + } + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/space/egress/cbor_gen.go b/protocol/commands/space/egress/cbor_gen.go new file mode 100644 index 00000000..3dde9ff9 --- /dev/null +++ b/protocol/commands/space/egress/cbor_gen.go @@ -0,0 +1,210 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package egress + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *TrackArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Endpoint (commands.CborURL) (struct) + if len("endpoint") > 8192 { + return xerrors.Errorf("Value in field \"endpoint\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("endpoint"))); err != nil { + return err + } + if _, err := cw.WriteString(string("endpoint")); err != nil { + return err + } + + if err := t.Endpoint.MarshalCBOR(cw); err != nil { + return err + } + + // t.Receipts (cid.Cid) (struct) + if len("receipts") > 8192 { + return xerrors.Errorf("Value in field \"receipts\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("receipts"))); err != nil { + return err + } + if _, err := cw.WriteString(string("receipts")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Receipts); err != nil { + return xerrors.Errorf("failed to write cid field t.Receipts: %w", err) + } + + return nil +} + +func (t *TrackArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = TrackArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("TrackArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 8) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Endpoint (commands.CborURL) (struct) + case "endpoint": + + { + + if err := t.Endpoint.UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Endpoint: %w", err) + } + + } + // t.Receipts (cid.Cid) (struct) + case "receipts": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Receipts: %w", err) + } + + t.Receipts = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *TrackOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{160}); err != nil { + return err + } + return nil +} + +func (t *TrackOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = TrackOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("TrackOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 0) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/space/egress/gen/main.go b/protocol/commands/space/egress/gen/main.go new file mode 100644 index 00000000..076c5528 --- /dev/null +++ b/protocol/commands/space/egress/gen/main.go @@ -0,0 +1,42 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/space/egress" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + egress.TrackArguments{}, + egress.TrackOK{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "egress", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "egress", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/space/egress/json_gen.go b/protocol/commands/space/egress/json_gen.go new file mode 100644 index 00000000..561133c5 --- /dev/null +++ b/protocol/commands/space/egress/json_gen.go @@ -0,0 +1,216 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package egress + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *TrackArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Endpoint (commands.CborURL) (struct) + if len("endpoint") > 8192 { + return fmt.Errorf("string in field \"endpoint\" was too long") + } + if err := jw.WriteString(string("endpoint")); err != nil { + return fmt.Errorf("writing string for field \"endpoint\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if err := t.Endpoint.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field t.Endpoint: %w", err) + } + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Receipts (cid.Cid) (struct) + if len("receipts") > 8192 { + return fmt.Errorf("string in field \"receipts\" was too long") + } + if err := jw.WriteString(string("receipts")); err != nil { + return fmt.Errorf("writing string for field \"receipts\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Receipts); err != nil { + return fmt.Errorf("writing CID for field t.Receipts: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *TrackArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = TrackArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for TrackArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for TrackArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for TrackArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field TrackArguments: string too large") + } + return fmt.Errorf("reading string for field TrackArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field TrackArguments: %w", err) + } + switch name { + + // t.Endpoint (commands.CborURL) (struct) + case "endpoint": + + if err := t.Endpoint.UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling t.Endpoint: %w", err) + } + + // t.Receipts (cid.Cid) (struct) + case "receipts": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Receipts: %w", err) + } + t.Receipts = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for TrackArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field TrackArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for TrackArguments") + } + } + } + + return nil +} +func (t *TrackOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *TrackOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = TrackOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for TrackOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for TrackOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for TrackOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field TrackOK: string too large") + } + return fmt.Errorf("reading string for field TrackOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field TrackOK: %w", err) + } + switch name { + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for TrackOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field TrackOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for TrackOK") + } + } + } + + return nil +} diff --git a/protocol/commands/space/egress/track.go b/protocol/commands/space/egress/track.go new file mode 100644 index 00000000..afb270ee --- /dev/null +++ b/protocol/commands/space/egress/track.go @@ -0,0 +1,15 @@ +//go:build !codegen + +package egress + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +// Track is the capability a storage node invokes to ask the egress +// tracking service to record egress accounted for in a batch of +// `/content/retrieve` receipts. The tracking service responds by forking +// a `/space/egress/consolidate` sub-invocation onto the receipt's +// effects; the typed OK return is empty. +var Track = binding.Bind[*TrackArguments, *TrackOK](command.MustParse("/space/egress/track")) diff --git a/protocol/commands/space/egress/types.go b/protocol/commands/space/egress/types.go new file mode 100644 index 00000000..5ee1dbec --- /dev/null +++ b/protocol/commands/space/egress/types.go @@ -0,0 +1,23 @@ +package egress + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/ipfs/go-cid" +) + +// TrackArguments is the argument shape of `/space/egress/track`. A storage +// node invokes this capability to ask the egress tracking service to record +// the egress accounted for in a batch of `/content/retrieve` receipts. +// Receipts is the CID of the receipts batch (root of the dag-cbor archive +// the storage node has staged); Endpoint is the URL the tracking service +// should fetch that archive from. +type TrackArguments struct { + Receipts cid.Cid `cborgen:"receipts" dagjsongen:"receipts"` + Endpoint commands.CborURL `cborgen:"endpoint" dagjsongen:"endpoint"` +} + +// TrackOK is the success return for `/space/egress/track`. The tracking +// service's consolidation response is delivered out-of-band as a forked +// sub-invocation (a `/space/egress/consolidate`) attached to the receipt's +// effects; the typed OK record itself is empty. +type TrackOK struct{} diff --git a/protocol/commands/space/gen/main.go b/protocol/commands/space/gen/main.go new file mode 100644 index 00000000..e446b50e --- /dev/null +++ b/protocol/commands/space/gen/main.go @@ -0,0 +1,41 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/space" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + space.InfoOK{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "space", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "space", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/space/info.go b/protocol/commands/space/info.go new file mode 100644 index 00000000..f1424e8f --- /dev/null +++ b/protocol/commands/space/info.go @@ -0,0 +1,15 @@ +//go:build !codegen + +package space + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type InfoArguments = commands.Unit + +var Info = binding.Bind[*InfoArguments, *InfoOK](command.MustParse("/space/info")) + +const UnknownSpaceErrorName = "UnknownSpace" diff --git a/protocol/commands/space/json_gen.go b/protocol/commands/space/json_gen.go new file mode 100644 index 00000000..7d68a872 --- /dev/null +++ b/protocol/commands/space/json_gen.go @@ -0,0 +1,166 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package space + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + did "github.com/fil-forge/ucantone/did" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *InfoOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Providers ([]did.DID) (slice) + if len("providers") > 8192 { + return fmt.Errorf("string in field \"providers\" was too long") + } + if err := jw.WriteString(string("providers")); err != nil { + return fmt.Errorf("writing string for field \"providers\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Providers) > 8192 { + return fmt.Errorf("slice value in field t.Providers was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Providers: %w", err) + } + for i, v := range t.Providers { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Providers: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Providers: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *InfoOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = InfoOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for InfoOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for InfoOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for InfoOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field InfoOK: string too large") + } + return fmt.Errorf("reading string for field InfoOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field InfoOK: %w", err) + } + switch name { + + // t.Providers ([]did.DID) (slice) + case "providers": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Providers: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Providers: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Providers: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]did.DID, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Providers = append(t.Providers, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Providers: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Providers: slice too large") + } + } + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for InfoOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field InfoOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for InfoOK") + } + } + } + + return nil +} diff --git a/protocol/commands/space/types.go b/protocol/commands/space/types.go new file mode 100644 index 00000000..a6e60289 --- /dev/null +++ b/protocol/commands/space/types.go @@ -0,0 +1,7 @@ +package space + +import "github.com/fil-forge/ucantone/did" + +type InfoOK struct { + Providers []did.DID `cborgen:"providers" dagjsongen:"providers"` +} diff --git a/protocol/commands/types.go b/protocol/commands/types.go new file mode 100644 index 00000000..4dded321 --- /dev/null +++ b/protocol/commands/types.go @@ -0,0 +1,9 @@ +// Package commands defines UCAN commands used across the forge +// stack. Each operation lives in a subpackage (e.g. blob, upload, pdp); +// this top-level package holds shared wire types and helpers. +package commands + +// Unit is the empty wire type returned by any capability whose receipt +// carries no payload (e.g. /upload/remove, /claim/cache). It encodes as +// an empty CBOR map / dag-json object. +type Unit struct{} diff --git a/protocol/commands/ucan/cbor_gen.go b/protocol/commands/ucan/cbor_gen.go new file mode 100644 index 00000000..8be5472a --- /dev/null +++ b/protocol/commands/ucan/cbor_gen.go @@ -0,0 +1,283 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package ucan + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *ConcludeArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Receipt (cid.Cid) (struct) + if len("receipt") > 8192 { + return xerrors.Errorf("Value in field \"receipt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("receipt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("receipt")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Receipt); err != nil { + return xerrors.Errorf("failed to write cid field t.Receipt: %w", err) + } + + return nil +} + +func (t *ConcludeArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ConcludeArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ConcludeArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Receipt (cid.Cid) (struct) + case "receipt": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Receipt: %w", err) + } + + t.Receipt = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RevokeArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{162}); err != nil { + return err + } + + // t.Path ([]cid.Cid) (slice) + if len("path") > 8192 { + return xerrors.Errorf("Value in field \"path\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("path"))); err != nil { + return err + } + if _, err := cw.WriteString(string("path")); err != nil { + return err + } + + if len(t.Path) > 8192 { + return xerrors.Errorf("Slice value in field t.Path was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Path))); err != nil { + return err + } + for _, v := range t.Path { + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + + // t.Revoke (cid.Cid) (struct) + if len("revoke") > 8192 { + return xerrors.Errorf("Value in field \"revoke\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("revoke"))); err != nil { + return err + } + if _, err := cw.WriteString(string("revoke")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Revoke); err != nil { + return xerrors.Errorf("failed to write cid field t.Revoke: %w", err) + } + + return nil +} + +func (t *RevokeArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RevokeArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RevokeArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Path ([]cid.Cid) (slice) + case "path": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Path: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Path = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Path[i]: %w", err) + } + + t.Path[i] = c + + } + + } + } + // t.Revoke (cid.Cid) (struct) + case "revoke": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Revoke: %w", err) + } + + t.Revoke = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/ucan/conclude.go b/protocol/commands/ucan/conclude.go new file mode 100644 index 00000000..59b23bb8 --- /dev/null +++ b/protocol/commands/ucan/conclude.go @@ -0,0 +1,18 @@ +//go:build !codegen + +package ucan + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" + "github.com/fil-forge/ucantone/ucan/command" +) + +type ConcludeOK = commands.Unit + +var Conclude = binding.Bind[*ConcludeArguments, *ConcludeOK](command.MustParse("/ucan/conclude")) + +const ConclusionReceiptNotFoundErrorName = "ConclusionReceiptNotFound" + +var ErrConclusionReceiptNotFound = errors.New(ConclusionReceiptNotFoundErrorName, "conclusion receipt not found") diff --git a/protocol/commands/ucan/gen/main.go b/protocol/commands/ucan/gen/main.go new file mode 100644 index 00000000..4bee06af --- /dev/null +++ b/protocol/commands/ucan/gen/main.go @@ -0,0 +1,42 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/ucan" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + ucan.ConcludeArguments{}, + ucan.RevokeArguments{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "ucan", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "ucan", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/ucan/json_gen.go b/protocol/commands/ucan/json_gen.go new file mode 100644 index 00000000..07763e60 --- /dev/null +++ b/protocol/commands/ucan/json_gen.go @@ -0,0 +1,303 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package ucan + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *ConcludeArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Receipt (cid.Cid) (struct) + if len("receipt") > 8192 { + return fmt.Errorf("string in field \"receipt\" was too long") + } + if err := jw.WriteString(string("receipt")); err != nil { + return fmt.Errorf("writing string for field \"receipt\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Receipt); err != nil { + return fmt.Errorf("writing CID for field t.Receipt: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ConcludeArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ConcludeArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ConcludeArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ConcludeArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ConcludeArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ConcludeArguments: string too large") + } + return fmt.Errorf("reading string for field ConcludeArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ConcludeArguments: %w", err) + } + switch name { + + // t.Receipt (cid.Cid) (struct) + case "receipt": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Receipt: %w", err) + } + t.Receipt = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ConcludeArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ConcludeArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ConcludeArguments") + } + } + } + + return nil +} +func (t *RevokeArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Path ([]cid.Cid) (slice) + if len("path") > 8192 { + return fmt.Errorf("string in field \"path\" was too long") + } + if err := jw.WriteString(string("path")); err != nil { + return fmt.Errorf("writing string for field \"path\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Path) > 8192 { + return fmt.Errorf("slice value in field t.Path was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Path: %w", err) + } + for i, v := range t.Path { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Path: %w", err) + } + } + + if err := jw.WriteCid(v); err != nil { + return fmt.Errorf("writing CID for field v: %w", err) + } + + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Path: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Revoke (cid.Cid) (struct) + if len("revoke") > 8192 { + return fmt.Errorf("string in field \"revoke\" was too long") + } + if err := jw.WriteString(string("revoke")); err != nil { + return fmt.Errorf("writing string for field \"revoke\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Revoke); err != nil { + return fmt.Errorf("writing CID for field t.Revoke: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RevokeArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RevokeArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RevokeArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RevokeArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RevokeArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RevokeArguments: string too large") + } + return fmt.Errorf("reading string for field RevokeArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RevokeArguments: %w", err) + } + switch name { + + // t.Path ([]cid.Cid) (slice) + case "path": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Path: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Path: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Path: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]cid.Cid, 1) + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field item[0]: %w", err) + } + item[0] = c + + } + t.Path = append(t.Path, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Path: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Path: slice too large") + } + } + } + + } + + // t.Revoke (cid.Cid) (struct) + case "revoke": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Revoke: %w", err) + } + t.Revoke = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RevokeArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RevokeArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RevokeArguments") + } + } + } + + return nil +} diff --git a/protocol/commands/ucan/revoke.go b/protocol/commands/ucan/revoke.go new file mode 100644 index 00000000..8e0f1576 --- /dev/null +++ b/protocol/commands/ucan/revoke.go @@ -0,0 +1,13 @@ +//go:build !codegen + +package ucan + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type RevokeOK = commands.Unit + +var Revoke = binding.Bind[*RevokeArguments, *RevokeOK](command.MustParse("/ucan/revoke")) diff --git a/protocol/commands/ucan/types.go b/protocol/commands/ucan/types.go new file mode 100644 index 00000000..9a6673c8 --- /dev/null +++ b/protocol/commands/ucan/types.go @@ -0,0 +1,16 @@ +package ucan + +import "github.com/ipfs/go-cid" + +type ConcludeArguments struct { + Receipt cid.Cid `cborgen:"receipt" dagjsongen:"receipt"` +} + +type RevokeArguments struct { + // Revoke is the CID of the UCAN delegation to revoke. + Revoke cid.Cid `cborgen:"revoke" dagjsongen:"revoke"` + // Path is the delegation path to the UCAN delegation to revoke. The path is a + // list of CIDs that represent the delegation chain from the root UCAN to the + // UCAN being revoked. + Path []cid.Cid `cborgen:"path" dagjsongen:"path"` +} diff --git a/protocol/commands/upload/add.go b/protocol/commands/upload/add.go new file mode 100644 index 00000000..0ad60645 --- /dev/null +++ b/protocol/commands/upload/add.go @@ -0,0 +1,13 @@ +//go:build !codegen + +package upload + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type AddOK = commands.Unit + +var Add = binding.Bind[*AddArguments, *AddOK](command.MustParse("/upload/add")) diff --git a/protocol/commands/upload/cbor_gen.go b/protocol/commands/upload/cbor_gen.go new file mode 100644 index 00000000..0593bd1e --- /dev/null +++ b/protocol/commands/upload/cbor_gen.go @@ -0,0 +1,884 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package upload + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *AddArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 3 + + if t.Index == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Root (cid.Cid) (struct) + if len("root") > 8192 { + return xerrors.Errorf("Value in field \"root\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("root"))); err != nil { + return err + } + if _, err := cw.WriteString(string("root")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Root); err != nil { + return xerrors.Errorf("failed to write cid field t.Root: %w", err) + } + + // t.Index (cid.Cid) (struct) + if t.Index != nil { + + if len("index") > 8192 { + return xerrors.Errorf("Value in field \"index\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("index"))); err != nil { + return err + } + if _, err := cw.WriteString(string("index")); err != nil { + return err + } + + if t.Index == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cbg.WriteCid(cw, *t.Index); err != nil { + return xerrors.Errorf("failed to write cid field t.Index: %w", err) + } + } + + } + + // t.Shards ([]cid.Cid) (slice) + if len("shards") > 8192 { + return xerrors.Errorf("Value in field \"shards\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("shards"))); err != nil { + return err + } + if _, err := cw.WriteString(string("shards")); err != nil { + return err + } + + if len(t.Shards) > 8192 { + return xerrors.Errorf("Slice value in field t.Shards was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Shards))); err != nil { + return err + } + for _, v := range t.Shards { + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + return nil +} + +func (t *AddArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = AddArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("AddArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Root (cid.Cid) (struct) + case "root": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Root: %w", err) + } + + t.Root = c + + } + // t.Index (cid.Cid) (struct) + case "index": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Index: %w", err) + } + + t.Index = &c + } + + } + // t.Shards ([]cid.Cid) (slice) + case "shards": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Shards: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Shards = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Shards[i]: %w", err) + } + + t.Shards[i] = c + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *RemoveArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Root (cid.Cid) (struct) + if len("root") > 8192 { + return xerrors.Errorf("Value in field \"root\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("root"))); err != nil { + return err + } + if _, err := cw.WriteString(string("root")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Root); err != nil { + return xerrors.Errorf("failed to write cid field t.Root: %w", err) + } + + return nil +} + +func (t *RemoveArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RemoveArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RemoveArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 4) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Root (cid.Cid) (struct) + case "root": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Root: %w", err) + } + + t.Root = c + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ListArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 2 + + if t.Cursor == nil { + fieldCount-- + } + + if t.Size == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Size (uint64) (uint64) + if t.Size != nil { + + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if t.Size == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.Size)); err != nil { + return err + } + } + + } + + // t.Cursor (string) (string) + if t.Cursor != nil { + + if len("cursor") > 8192 { + return xerrors.Errorf("Value in field \"cursor\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cursor"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cursor")); err != nil { + return err + } + + if t.Cursor == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Cursor) > 8192 { + return xerrors.Errorf("Value in field t.Cursor was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Cursor))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Cursor)); err != nil { + return err + } + } + } + return nil +} + +func (t *ListArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Size (uint64) (uint64) + case "size": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + typed := uint64(extra) + t.Size = &typed + } + + } + // t.Cursor (string) (string) + case "cursor": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Cursor = (*string)(&sval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ListOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 3 + + if t.Cursor == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil { + return err + } + + // t.Cursor (string) (string) + if t.Cursor != nil { + + if len("cursor") > 8192 { + return xerrors.Errorf("Value in field \"cursor\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cursor"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cursor")); err != nil { + return err + } + + if t.Cursor == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Cursor) > 8192 { + return xerrors.Errorf("Value in field t.Cursor was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Cursor))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Cursor)); err != nil { + return err + } + } + } + + // t.Results ([]upload.ListUploadItem) (slice) + if len("results") > 8192 { + return xerrors.Errorf("Value in field \"results\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("results"))); err != nil { + return err + } + if _, err := cw.WriteString(string("results")); err != nil { + return err + } + + if len(t.Results) > 8192 { + return xerrors.Errorf("Slice value in field t.Results was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Results))); err != nil { + return err + } + for _, v := range t.Results { + if err := v.MarshalCBOR(cw); err != nil { + return err + } + + } + return nil +} + +func (t *ListOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Size (uint64) (uint64) + case "size": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Size = uint64(extra) + + } + // t.Cursor (string) (string) + case "cursor": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Cursor = (*string)(&sval) + } + } + // t.Results ([]upload.ListUploadItem) (slice) + case "results": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Results: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Results = make([]ListUploadItem, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + if err := t.Results[i].UnmarshalCBOR(cr); err != nil { + return xerrors.Errorf("unmarshaling t.Results[i]: %w", err) + } + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ListUploadItem) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 2 + + if t.Index == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Root (cid.Cid) (struct) + if len("root") > 8192 { + return xerrors.Errorf("Value in field \"root\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("root"))); err != nil { + return err + } + if _, err := cw.WriteString(string("root")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Root); err != nil { + return xerrors.Errorf("failed to write cid field t.Root: %w", err) + } + + // t.Index (cid.Cid) (struct) + if t.Index != nil { + + if len("index") > 8192 { + return xerrors.Errorf("Value in field \"index\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("index"))); err != nil { + return err + } + if _, err := cw.WriteString(string("index")); err != nil { + return err + } + + if t.Index == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cbg.WriteCid(cw, *t.Index); err != nil { + return xerrors.Errorf("failed to write cid field t.Index: %w", err) + } + } + + } + return nil +} + +func (t *ListUploadItem) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListUploadItem{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListUploadItem: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 5) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Root (cid.Cid) (struct) + case "root": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Root: %w", err) + } + + t.Root = c + + } + // t.Index (cid.Cid) (struct) + case "index": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Index: %w", err) + } + + t.Index = &c + } + + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/upload/gen/main.go b/protocol/commands/upload/gen/main.go new file mode 100644 index 00000000..45b494ba --- /dev/null +++ b/protocol/commands/upload/gen/main.go @@ -0,0 +1,45 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/upload" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + upload.AddArguments{}, + upload.RemoveArguments{}, + upload.ListArguments{}, + upload.ListOK{}, + upload.ListUploadItem{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "upload", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "upload", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/upload/json_gen.go b/protocol/commands/upload/json_gen.go new file mode 100644 index 00000000..a7813d36 --- /dev/null +++ b/protocol/commands/upload/json_gen.go @@ -0,0 +1,866 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package upload + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *AddArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Index (cid.Cid) (struct) + if t.Index != nil { + if len("index") > 8192 { + return fmt.Errorf("string in field \"index\" was too long") + } + if err := jw.WriteString(string("index")); err != nil { + return fmt.Errorf("writing string for field \"index\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if t.Index == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Index: %w", err) + } + } else { + if err := jw.WriteCid(*t.Index); err != nil { + return fmt.Errorf("writing CID for field t.Index: %w", err) + } + } + + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Root (cid.Cid) (struct) + if len("root") > 8192 { + return fmt.Errorf("string in field \"root\" was too long") + } + if err := jw.WriteString(string("root")); err != nil { + return fmt.Errorf("writing string for field \"root\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Root); err != nil { + return fmt.Errorf("writing CID for field t.Root: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Shards ([]cid.Cid) (slice) + if len("shards") > 8192 { + return fmt.Errorf("string in field \"shards\" was too long") + } + if err := jw.WriteString(string("shards")); err != nil { + return fmt.Errorf("writing string for field \"shards\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Shards) > 8192 { + return fmt.Errorf("slice value in field t.Shards was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Shards: %w", err) + } + for i, v := range t.Shards { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Shards: %w", err) + } + } + + if err := jw.WriteCid(v); err != nil { + return fmt.Errorf("writing CID for field v: %w", err) + } + + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Shards: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *AddArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = AddArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for AddArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for AddArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for AddArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field AddArguments: string too large") + } + return fmt.Errorf("reading string for field AddArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field AddArguments: %w", err) + } + switch name { + + // t.Index (cid.Cid) (struct) + case "index": + { + + c, err := jr.ReadCidOrNull() + if err != nil { + return fmt.Errorf("reading CID or null for field t.Index: %w", err) + } + t.Index = c + + } + + // t.Root (cid.Cid) (struct) + case "root": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Root: %w", err) + } + t.Root = c + + } + + // t.Shards ([]cid.Cid) (slice) + case "shards": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Shards: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Shards: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Shards: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]cid.Cid, 1) + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field item[0]: %w", err) + } + item[0] = c + + } + t.Shards = append(t.Shards, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Shards: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Shards: slice too large") + } + } + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for AddArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field AddArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for AddArguments") + } + } + } + + return nil +} +func (t *RemoveArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + + // t.Root (cid.Cid) (struct) + if len("root") > 8192 { + return fmt.Errorf("string in field \"root\" was too long") + } + if err := jw.WriteString(string("root")); err != nil { + return fmt.Errorf("writing string for field \"root\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Root); err != nil { + return fmt.Errorf("writing CID for field t.Root: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RemoveArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RemoveArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RemoveArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RemoveArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RemoveArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RemoveArguments: string too large") + } + return fmt.Errorf("reading string for field RemoveArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RemoveArguments: %w", err) + } + switch name { + + // t.Root (cid.Cid) (struct) + case "root": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Root: %w", err) + } + t.Root = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RemoveArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RemoveArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RemoveArguments") + } + } + } + + return nil +} +func (t *ListArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Cursor (string) (string) + if t.Cursor != nil { + if len("cursor") > 8192 { + return fmt.Errorf("string in field \"cursor\" was too long") + } + if err := jw.WriteString(string("cursor")); err != nil { + return fmt.Errorf("writing string for field \"cursor\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if t.Cursor == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Cursor: %w", err) + } + } else { + if len(*t.Cursor) > 8192 { + return fmt.Errorf("string in field t.Cursor was too long") + } + if err := jw.WriteString(string(*t.Cursor)); err != nil { + return fmt.Errorf("writing string for field t.Cursor: %w", err) + } + } + written = true + } + if t.Size != nil { + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + } + + // t.Size (uint64) (uint64) + if t.Size != nil { + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if t.Size == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Size: %w", err) + } + } else { + if err := jw.WriteUint64(uint64(*t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + } + + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListArguments: string too large") + } + return fmt.Errorf("reading string for field ListArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListArguments: %w", err) + } + switch name { + + // t.Cursor (string) (string) + case "cursor": + { + sval, err := jr.ReadStringOrNull(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string or null for field t.Cursor: string too long") + } + return fmt.Errorf("reading string or null for field t.Cursor: %w", err) + } + if sval != nil { + t.Cursor = (*string)(sval) + } + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64OrNull() + if err != nil { + return fmt.Errorf("reading uint64 or null for field t.Size: %w", err) + } + if nval != nil { + typed := uint64(*nval) + t.Size = &typed + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListArguments") + } + } + } + + return nil +} +func (t *ListOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Cursor (string) (string) + if t.Cursor != nil { + if len("cursor") > 8192 { + return fmt.Errorf("string in field \"cursor\" was too long") + } + if err := jw.WriteString(string("cursor")); err != nil { + return fmt.Errorf("writing string for field \"cursor\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if t.Cursor == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Cursor: %w", err) + } + } else { + if len(*t.Cursor) > 8192 { + return fmt.Errorf("string in field t.Cursor was too long") + } + if err := jw.WriteString(string(*t.Cursor)); err != nil { + return fmt.Errorf("writing string for field t.Cursor: %w", err) + } + } + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Results ([]upload.ListUploadItem) (slice) + if len("results") > 8192 { + return fmt.Errorf("string in field \"results\" was too long") + } + if err := jw.WriteString(string("results")); err != nil { + return fmt.Errorf("writing string for field \"results\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Results) > 8192 { + return fmt.Errorf("slice value in field t.Results was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Results: %w", err) + } + for i, v := range t.Results { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Results: %w", err) + } + } + if err := v.MarshalDagJSON(jw); err != nil { + return fmt.Errorf("marshaling field v: %w", err) + } + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Results: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListOK: string too large") + } + return fmt.Errorf("reading string for field ListOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListOK: %w", err) + } + switch name { + + // t.Cursor (string) (string) + case "cursor": + { + sval, err := jr.ReadStringOrNull(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string or null for field t.Cursor: string too long") + } + return fmt.Errorf("reading string or null for field t.Cursor: %w", err) + } + if sval != nil { + t.Cursor = (*string)(sval) + } + } + + // t.Results ([]upload.ListUploadItem) (slice) + case "results": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Results: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Results: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Results: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]ListUploadItem, 1) + + if err := item[0].UnmarshalDagJSON(jr); err != nil { + return fmt.Errorf("unmarshaling item[0]: %w", err) + } + + t.Results = append(t.Results, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Results: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Results: slice too large") + } + } + } + + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Size: %w", err) + } + t.Size = uint64(nval) + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListOK") + } + } + } + + return nil +} +func (t *ListUploadItem) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Index (cid.Cid) (struct) + if t.Index != nil { + if len("index") > 8192 { + return fmt.Errorf("string in field \"index\" was too long") + } + if err := jw.WriteString(string("index")); err != nil { + return fmt.Errorf("writing string for field \"index\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if t.Index == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Index: %w", err) + } + } else { + if err := jw.WriteCid(*t.Index); err != nil { + return fmt.Errorf("writing CID for field t.Index: %w", err) + } + } + + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Root (cid.Cid) (struct) + if len("root") > 8192 { + return fmt.Errorf("string in field \"root\" was too long") + } + if err := jw.WriteString(string("root")); err != nil { + return fmt.Errorf("writing string for field \"root\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Root); err != nil { + return fmt.Errorf("writing CID for field t.Root: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListUploadItem) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListUploadItem{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListUploadItem: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListUploadItem: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListUploadItem: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListUploadItem: string too large") + } + return fmt.Errorf("reading string for field ListUploadItem: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListUploadItem: %w", err) + } + switch name { + + // t.Index (cid.Cid) (struct) + case "index": + { + + c, err := jr.ReadCidOrNull() + if err != nil { + return fmt.Errorf("reading CID or null for field t.Index: %w", err) + } + t.Index = c + + } + + // t.Root (cid.Cid) (struct) + case "root": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Root: %w", err) + } + t.Root = c + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListUploadItem: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListUploadItem: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListUploadItem") + } + } + } + + return nil +} diff --git a/protocol/commands/upload/list.go b/protocol/commands/upload/list.go new file mode 100644 index 00000000..7be55aed --- /dev/null +++ b/protocol/commands/upload/list.go @@ -0,0 +1,10 @@ +//go:build !codegen + +package upload + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +var List = binding.Bind[*ListArguments, *ListOK](command.MustParse("/upload/list")) diff --git a/protocol/commands/upload/remove.go b/protocol/commands/upload/remove.go new file mode 100644 index 00000000..b52e2dc3 --- /dev/null +++ b/protocol/commands/upload/remove.go @@ -0,0 +1,18 @@ +//go:build !codegen + +package upload + +import ( + "github.com/fil-forge/forge/protocol/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type RemoveOK = commands.Unit + +// Remove (/upload/remove) deletes an upload's root→shards index entry on the +// upload service (subject = the space). It does NOT remove the shard blobs — +// blob removal is a separate per-digest /blob/remove decision owned by the +// client's reference accounting. Idempotent: removing an unknown root +// succeeds. The receipt carries no payload (Unit). +var Remove = binding.Bind[*RemoveArguments, *RemoveOK](command.MustParse("/upload/remove")) diff --git a/protocol/commands/upload/shard/cbor_gen.go b/protocol/commands/upload/shard/cbor_gen.go new file mode 100644 index 00000000..4fd50e96 --- /dev/null +++ b/protocol/commands/upload/shard/cbor_gen.go @@ -0,0 +1,454 @@ +//go:build !codegen + +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package shard + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *ListArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 3 + + if t.Cursor == nil { + fieldCount-- + } + + if t.Size == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Root (cid.Cid) (struct) + if len("root") > 8192 { + return xerrors.Errorf("Value in field \"root\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("root"))); err != nil { + return err + } + if _, err := cw.WriteString(string("root")); err != nil { + return err + } + + if err := cbg.WriteCid(cw, t.Root); err != nil { + return xerrors.Errorf("failed to write cid field t.Root: %w", err) + } + + // t.Size (uint64) (uint64) + if t.Size != nil { + + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if t.Size == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(*t.Size)); err != nil { + return err + } + } + + } + + // t.Cursor (string) (string) + if t.Cursor != nil { + + if len("cursor") > 8192 { + return xerrors.Errorf("Value in field \"cursor\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cursor"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cursor")); err != nil { + return err + } + + if t.Cursor == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Cursor) > 8192 { + return xerrors.Errorf("Value in field t.Cursor was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Cursor))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Cursor)); err != nil { + return err + } + } + } + return nil +} + +func (t *ListArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Root (cid.Cid) (struct) + case "root": + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Root: %w", err) + } + + t.Root = c + + } + // t.Size (uint64) (uint64) + case "size": + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + typed := uint64(extra) + t.Size = &typed + } + + } + // t.Cursor (string) (string) + case "cursor": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Cursor = (*string)(&sval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ListOK) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + fieldCount := 3 + + if t.Cursor == nil { + fieldCount-- + } + + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { + return err + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return xerrors.Errorf("Value in field \"size\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("size"))); err != nil { + return err + } + if _, err := cw.WriteString(string("size")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Size)); err != nil { + return err + } + + // t.Cursor (string) (string) + if t.Cursor != nil { + + if len("cursor") > 8192 { + return xerrors.Errorf("Value in field \"cursor\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("cursor"))); err != nil { + return err + } + if _, err := cw.WriteString(string("cursor")); err != nil { + return err + } + + if t.Cursor == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if len(*t.Cursor) > 8192 { + return xerrors.Errorf("Value in field t.Cursor was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Cursor))); err != nil { + return err + } + if _, err := cw.WriteString(string(*t.Cursor)); err != nil { + return err + } + } + } + + // t.Results ([]cid.Cid) (slice) + if len("results") > 8192 { + return xerrors.Errorf("Value in field \"results\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("results"))); err != nil { + return err + } + if _, err := cw.WriteString(string("results")); err != nil { + return err + } + + if len(t.Results) > 8192 { + return xerrors.Errorf("Slice value in field t.Results was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Results))); err != nil { + return err + } + for _, v := range t.Results { + + if err := cbg.WriteCid(cw, v); err != nil { + return xerrors.Errorf("failed to write cid field v: %w", err) + } + + } + return nil +} + +func (t *ListOK) UnmarshalCBOR(r io.Reader) (err error) { + *t = ListOK{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("ListOK: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 7) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Size (uint64) (uint64) + case "size": + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.Size = uint64(extra) + + } + // t.Cursor (string) (string) + case "cursor": + + { + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Cursor = (*string)(&sval) + } + } + // t.Results ([]cid.Cid) (slice) + case "results": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Results: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Results = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + _ = maj + _ = extra + _ = err + + { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Results[i]: %w", err) + } + + t.Results[i] = c + + } + + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/protocol/commands/upload/shard/gen/main.go b/protocol/commands/upload/shard/gen/main.go new file mode 100644 index 00000000..110de2f3 --- /dev/null +++ b/protocol/commands/upload/shard/gen/main.go @@ -0,0 +1,42 @@ +//go:generate go run -tags codegen . + +package main + +import ( + "os" + + jsg "github.com/alanshaw/dag-json-gen" + "github.com/fil-forge/forge/protocol/commands/upload/shard" + cbg "github.com/whyrusleeping/cbor-gen" +) + +const buildTag = "//go:build !codegen\n\n" + +func tag(path string) { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + if err := os.WriteFile(path, append([]byte(buildTag), data...), 0644); err != nil { + panic(err) + } +} + +func main() { + models := []any{ + shard.ListArguments{}, + shard.ListOK{}, + } + const ( + cborFile = "../cbor_gen.go" + jsonFile = "../json_gen.go" + ) + if err := cbg.WriteMapEncodersToFile(cborFile, "shard", models...); err != nil { + panic(err) + } + if err := jsg.WriteMapEncodersToFile(jsonFile, "shard", models...); err != nil { + panic(err) + } + tag(cborFile) + tag(jsonFile) +} diff --git a/protocol/commands/upload/shard/json_gen.go b/protocol/commands/upload/shard/json_gen.go new file mode 100644 index 00000000..d25c1f7a --- /dev/null +++ b/protocol/commands/upload/shard/json_gen.go @@ -0,0 +1,443 @@ +//go:build !codegen + +// Code generated by github.com/alanshaw/dag-json-gen. DO NOT EDIT. + +package shard + +import ( + "errors" + "fmt" + "io" + "math" + "sort" + + jsg "github.com/alanshaw/dag-json-gen" + cid "github.com/ipfs/go-cid" +) + +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort +var _ = errors.Is + +func (t *ListArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Cursor (string) (string) + if t.Cursor != nil { + if len("cursor") > 8192 { + return fmt.Errorf("string in field \"cursor\" was too long") + } + if err := jw.WriteString(string("cursor")); err != nil { + return fmt.Errorf("writing string for field \"cursor\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if t.Cursor == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Cursor: %w", err) + } + } else { + if len(*t.Cursor) > 8192 { + return fmt.Errorf("string in field t.Cursor was too long") + } + if err := jw.WriteString(string(*t.Cursor)); err != nil { + return fmt.Errorf("writing string for field t.Cursor: %w", err) + } + } + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Root (cid.Cid) (struct) + if len("root") > 8192 { + return fmt.Errorf("string in field \"root\" was too long") + } + if err := jw.WriteString(string("root")); err != nil { + return fmt.Errorf("writing string for field \"root\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteCid(t.Root); err != nil { + return fmt.Errorf("writing CID for field t.Root: %w", err) + } + + written = true + if t.Size != nil { + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + } + + // t.Size (uint64) (uint64) + if t.Size != nil { + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if t.Size == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Size: %w", err) + } + } else { + if err := jw.WriteUint64(uint64(*t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + } + + } + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListArguments: string too large") + } + return fmt.Errorf("reading string for field ListArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListArguments: %w", err) + } + switch name { + + // t.Cursor (string) (string) + case "cursor": + { + sval, err := jr.ReadStringOrNull(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string or null for field t.Cursor: string too long") + } + return fmt.Errorf("reading string or null for field t.Cursor: %w", err) + } + if sval != nil { + t.Cursor = (*string)(sval) + } + } + + // t.Root (cid.Cid) (struct) + case "root": + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field t.Root: %w", err) + } + t.Root = c + + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64OrNull() + if err != nil { + return fmt.Errorf("reading uint64 or null for field t.Size: %w", err) + } + if nval != nil { + typed := uint64(*nval) + t.Size = &typed + } + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListArguments") + } + } + } + + return nil +} +func (t *ListOK) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } + written := false + + // t.Cursor (string) (string) + if t.Cursor != nil { + if len("cursor") > 8192 { + return fmt.Errorf("string in field \"cursor\" was too long") + } + if err := jw.WriteString(string("cursor")); err != nil { + return fmt.Errorf("writing string for field \"cursor\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if t.Cursor == nil { + if err := jw.WriteNull(); err != nil { + return fmt.Errorf("writing null for field t.Cursor: %w", err) + } + } else { + if len(*t.Cursor) > 8192 { + return fmt.Errorf("string in field t.Cursor was too long") + } + if err := jw.WriteString(string(*t.Cursor)); err != nil { + return fmt.Errorf("writing string for field t.Cursor: %w", err) + } + } + written = true + } + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Results ([]cid.Cid) (slice) + if len("results") > 8192 { + return fmt.Errorf("string in field \"results\" was too long") + } + if err := jw.WriteString(string("results")); err != nil { + return fmt.Errorf("writing string for field \"results\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Results) > 8192 { + return fmt.Errorf("slice value in field t.Results was too long") + } + + if err := jw.WriteArrayOpen(); err != nil { + return fmt.Errorf("writing array open for field t.Results: %w", err) + } + for i, v := range t.Results { + if i > 0 { + if err := jw.WriteComma(); err != nil { + return fmt.Errorf("writing comma for field t.Results: %w", err) + } + } + + if err := jw.WriteCid(v); err != nil { + return fmt.Errorf("writing CID for field v: %w", err) + } + + } + if err := jw.WriteArrayClose(); err != nil { + return fmt.Errorf("writing array close for field t.Results: %w", err) + } + + written = true + if written { + if err := jw.WriteComma(); err != nil { + return err + } + } + + // t.Size (uint64) (uint64) + if len("size") > 8192 { + return fmt.Errorf("string in field \"size\" was too long") + } + if err := jw.WriteString(string("size")); err != nil { + return fmt.Errorf("writing string for field \"size\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + + if err := jw.WriteUint64(uint64(t.Size)); err != nil { + return fmt.Errorf("writing uint64 for field t.Size: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *ListOK) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ListOK{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for ListOK: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for ListOK: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for ListOK: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field ListOK: string too large") + } + return fmt.Errorf("reading string for field ListOK: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field ListOK: %w", err) + } + switch name { + + // t.Cursor (string) (string) + case "cursor": + { + sval, err := jr.ReadStringOrNull(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string or null for field t.Cursor: string too long") + } + return fmt.Errorf("reading string or null for field t.Cursor: %w", err) + } + if sval != nil { + t.Cursor = (*string)(sval) + } + } + + // t.Results ([]cid.Cid) (slice) + case "results": + { + + if err := jr.ReadArrayOpen(); err != nil { + return fmt.Errorf("reading array open for field t.Results: %w", err) + } + + close, err := jr.PeekArrayClose() + if err != nil { + return fmt.Errorf("peeking array close for field t.Results: %w", err) + } + if close { + if err := jr.ReadArrayClose(); err != nil { + return fmt.Errorf("reading array close for field t.Results: %w", err) + } + + } else { + for i := 0; i < 8192; i++ { + item := make([]cid.Cid, 1) + { + + c, err := jr.ReadCid() + if err != nil { + return fmt.Errorf("reading CID for field item[0]: %w", err) + } + item[0] = c + + } + t.Results = append(t.Results, item[0]) + + close, err := jr.ReadArrayCloseOrComma() + if err != nil { + return fmt.Errorf("reading array close or comma for field t.Results: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("reading array for field t.Results: slice too large") + } + } + } + + } + + // t.Size (uint64) (uint64) + case "size": + { + + nval, err := jr.ReadNumberAsUint64() + if err != nil { + return fmt.Errorf("reading uint64 for field t.Size: %w", err) + } + t.Size = uint64(nval) + + } + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for ListOK: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field ListOK: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for ListOK") + } + } + } + + return nil +} diff --git a/protocol/commands/upload/shard/list.go b/protocol/commands/upload/shard/list.go new file mode 100644 index 00000000..1bb7ea5f --- /dev/null +++ b/protocol/commands/upload/shard/list.go @@ -0,0 +1,10 @@ +//go:build !codegen + +package shard + +import ( + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +var List = binding.Bind[*ListArguments, *ListOK](command.MustParse("/upload/shard/list")) diff --git a/protocol/commands/upload/shard/types.go b/protocol/commands/upload/shard/types.go new file mode 100644 index 00000000..a7a19990 --- /dev/null +++ b/protocol/commands/upload/shard/types.go @@ -0,0 +1,15 @@ +package shard + +import "github.com/ipfs/go-cid" + +type ListArguments struct { + Root cid.Cid `cborgen:"root" dagjsongen:"root"` + Cursor *string `cborgen:"cursor,omitempty" dagjsongen:"cursor,omitempty"` + Size *uint64 `cborgen:"size,omitempty" dagjsongen:"size,omitempty"` +} + +type ListOK struct { + Cursor *string `cborgen:"cursor,omitempty" dagjsongen:"cursor,omitempty"` + Size uint64 `cborgen:"size" dagjsongen:"size"` + Results []cid.Cid `cborgen:"results" dagjsongen:"results"` +} diff --git a/protocol/commands/upload/types.go b/protocol/commands/upload/types.go new file mode 100644 index 00000000..bc6582dc --- /dev/null +++ b/protocol/commands/upload/types.go @@ -0,0 +1,29 @@ +package upload + +import "github.com/ipfs/go-cid" + +type AddArguments struct { + Root cid.Cid `cborgen:"root" dagjsongen:"root"` + Shards []cid.Cid `cborgen:"shards" dagjsongen:"shards"` + Index *cid.Cid `cborgen:"index,omitempty" dagjsongen:"index,omitempty"` +} + +type RemoveArguments struct { + Root cid.Cid `cborgen:"root" dagjsongen:"root"` +} + +type ListArguments struct { + Cursor *string `cborgen:"cursor,omitempty" dagjsongen:"cursor,omitempty"` + Size *uint64 `cborgen:"size,omitempty" dagjsongen:"size,omitempty"` +} + +type ListOK struct { + Cursor *string `cborgen:"cursor,omitempty" dagjsongen:"cursor,omitempty"` + Size uint64 `cborgen:"size" dagjsongen:"size"` + Results []ListUploadItem `cborgen:"results" dagjsongen:"results"` +} + +type ListUploadItem struct { + Root cid.Cid `cborgen:"root" dagjsongen:"root"` + Index *cid.Cid `cborgen:"index,omitempty" dagjsongen:"index,omitempty"` +} diff --git a/protocol/digestutil/format.go b/protocol/digestutil/format.go new file mode 100644 index 00000000..bc7ec710 --- /dev/null +++ b/protocol/digestutil/format.go @@ -0,0 +1,25 @@ +package digestutil + +import ( + "fmt" + + "github.com/multiformats/go-multibase" + "github.com/multiformats/go-multihash" +) + +func Format(digest multihash.Multihash) string { + key, _ := multibase.Encode(multibase.Base58BTC, digest) + return key +} + +func Parse(input string) (multihash.Multihash, error) { + _, bytes, err := multibase.Decode(input) + if err != nil { + return nil, fmt.Errorf("decoding multibase encoded digest: %s", err) + } + digest, err := multihash.Cast(bytes) + if err != nil { + return nil, fmt.Errorf("invalid multihash digest: %s", err) + } + return digest, nil +} diff --git a/protocol/go.mod b/protocol/go.mod new file mode 100644 index 00000000..4db8aeba --- /dev/null +++ b/protocol/go.mod @@ -0,0 +1,39 @@ +module github.com/fil-forge/forge/protocol + +go 1.27.0 + +require ( + github.com/alanshaw/dag-json-gen v0.0.9 + github.com/fil-forge/automobile v0.0.1 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 + github.com/filecoin-project/go-data-segment v0.0.1 + github.com/ipfs/go-cid v0.6.2 + github.com/ipfs/go-log/v2 v2.9.2 + github.com/multiformats/go-multibase v0.3.0 + github.com/multiformats/go-multihash v0.2.3 + github.com/stretchr/testify v1.12.1 + github.com/whyrusleeping/cbor-gen v0.3.1 + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da +) + +require ( + github.com/filecoin-project/go-fil-commcid v0.3.1 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/mr-tron/base58 v1.3.0 // indirect + github.com/multiformats/go-base32 v0.1.0 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.10.0 // indirect + github.com/multiformats/go-varint v0.1.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect + golang.org/x/sys v0.47.0 // indirect + lukechampine.com/blake3 v1.4.1 // indirect + pitr.ca/jsontokenizer v0.3.2 // indirect +) diff --git a/protocol/go.sum b/protocol/go.sum new file mode 100644 index 00000000..11cc795d --- /dev/null +++ b/protocol/go.sum @@ -0,0 +1,68 @@ +github.com/alanshaw/dag-json-gen v0.0.9 h1:q59Ra1mQB6HnFGI1ilFxsZvF1CqGN0XDlUWq16EJ6pw= +github.com/alanshaw/dag-json-gen v0.0.9/go.mod h1:v1YBZcS4B355MqxtyQr+fGNbEhm0CzHd+gOqOO/MZ+I= +github.com/fil-forge/automobile v0.0.1 h1:9xB3yc4l5b9EdRJSJcNwudgBFNHoMPEAdcb7GfobLhA= +github.com/fil-forge/automobile v0.0.1/go.mod h1:TsO7jlO8ykJZY5tF8j4GsUcu3F02lEzxO7ULoB61hRA= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +github.com/filecoin-project/go-data-segment v0.0.1 h1:1wmDxOG4ubWQm3ZC1XI5nCon5qgSq7Ra3Rb6Dbu10Gs= +github.com/filecoin-project/go-data-segment v0.0.1/go.mod h1:H0/NKbsRxmRFBcLibmABv+yFNHdmtl5AyplYLnb0Zv4= +github.com/filecoin-project/go-fil-commcid v0.3.1 h1:4EfxpHSlvtkOqa9weG2Yt5kxFmPib2xU7Uc9Lbqk7fs= +github.com/filecoin-project/go-fil-commcid v0.3.1/go.mod h1:z7Ssf8d7kspF9QRAVHDbZ+43JK4mkhbGH5lyph1TnKY= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= +github.com/ipfs/go-log/v2 v2.9.2 h1:O/5BB0elpkRILvT24rCJ5976wWd7u0nJ436T3rdYdc4= +github.com/ipfs/go-log/v2 v2.9.2/go.mod h1:RziRwwXWhndlk8L75RnEe0zeAYaq2heKtEMc3jqUov0= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= +github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= +github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= +github.com/multiformats/go-multibase v0.3.0/go.mod h1:MoBLQPCkRTOL3eveIPO81860j2AQY8JwcnNlRkGRUfI= +github.com/multiformats/go-multicodec v0.10.0 h1:UpP223cig/Cx8J76jWt91njpK3GTAO1w02sdcjZDSuc= +github.com/multiformats/go-multicodec v0.10.0/go.mod h1:wg88pM+s2kZJEQfRCKBNU+g32F5aWBEjyFHXvZLTcLI= +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-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= +github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= +gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= +gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= +lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= +pitr.ca/jsontokenizer v0.3.2 h1:kzsKwfkWPV5XCmAF//hznh3L9IXbiL8aHbI0FswRkCs= +pitr.ca/jsontokenizer v0.3.2/go.mod h1:3DJdA2QNOU6cI0XkH6pRKZ4Oe8G5SDRUQ6PFAwaQ3YY= diff --git a/protocol/receipt/client.go b/protocol/receipt/client.go new file mode 100644 index 00000000..00dd90f9 --- /dev/null +++ b/protocol/receipt/client.go @@ -0,0 +1,158 @@ +package receipt + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/fil-forge/ucantone/transport" + "github.com/fil-forge/ucantone/ucan" + "github.com/ipfs/go-cid" +) + +type ResponseDecoder[Res any] interface { + Decode(Res) (ucan.Container, error) +} + +var ErrNotFound = errors.New("receipt not found") + +var ( + PollInterval = time.Second + PollRetries = 10 +) + +type Client struct { + endpoint *url.URL + client *http.Client + codec ResponseDecoder[*http.Response] +} + +type Option func(c *Client) + +func WithCodec(codec ResponseDecoder[*http.Response]) Option { + return func(c *Client) { + c.codec = codec + } +} + +func WithHTTPClient(client *http.Client) Option { + return func(c *Client) { + c.client = client + } +} + +func NewClient(endpoint *url.URL, options ...Option) *Client { + c := Client{ + endpoint: endpoint, + codec: transport.DefaultHTTPOutboundCodec, + } + for _, o := range options { + o(&c) + } + if c.client == nil { + c.client = http.DefaultClient + } + return &c +} + +// Fetch a receipt from the receipt API. Returns [ErrNotFound] if the API +// responds with [http.StatusNotFound]. +func (c *Client) Fetch(ctx context.Context, task cid.Cid) (ucan.Receipt, ucan.Container, error) { + receiptURL := c.endpoint.JoinPath(task.String()) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, receiptURL.String(), nil) + if err != nil { + return nil, nil, fmt.Errorf("creating get request: %w", err) + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("doing receipts request: %w", err) + } + defer resp.Body.Close() + + var ct ucan.Container + switch resp.StatusCode { + case http.StatusOK: + ct, err = c.codec.Decode(resp) + if err != nil { + return nil, nil, fmt.Errorf("decoding message: %w", err) + } + case http.StatusNotFound: + return nil, nil, ErrNotFound + default: + return nil, nil, fmt.Errorf("unexpected status: %s", resp.Status) + } + + rcpt, ok := ct.Receipt(task) + if !ok { + return nil, nil, errors.New("receipt not found in UCAN container") + } + + return rcpt, ct, nil +} + +type pollConfig struct { + interval *time.Duration + retries *int +} + +type PollOption func(opt *pollConfig) + +// WithInterval configures the time to wait between poll requests. The default +// is [PollInterval]. +func WithInterval(interval time.Duration) PollOption { + return func(opt *pollConfig) { + opt.interval = &interval + } +} + +// WithRetries configures the maximum number of times that Poll will attempt to +// fetch a receipt. The default is [PollRetries] requests. Set it to -1 to poll +// until a non-404 response is encountered. +func WithRetries(n int) PollOption { + return func(opt *pollConfig) { + opt.retries = &n + } +} + +// Poll attempts to fetch a receipt from the endpoint until a non-404 response +// is encountered or until the configured maximum retries are made. +func (c *Client) Poll(ctx context.Context, task cid.Cid, options ...PollOption) (ucan.Receipt, ucan.Container, error) { + conf := pollConfig{} + for _, o := range options { + o(&conf) + } + if conf.interval == nil { + conf.interval = &PollInterval + } + if conf.retries == nil { + conf.retries = &PollRetries + } + + attempts := 0 + for { + rcpt, ct, err := c.Fetch(ctx, task) + if err != nil && !errors.Is(err, ErrNotFound) { + return nil, nil, err + } + if err == nil { + return rcpt, ct, nil + } + + attempts++ + if *conf.retries > -1 && (attempts-1) >= *conf.retries { + return nil, nil, fmt.Errorf("receipt for %s was not found after %d attempts", task, attempts) + } + + // wait for the configured interval, or the context to be canceled + sleep, cancel := context.WithTimeout(ctx, *conf.interval) + <-sleep.Done() + cancel() + if ctx.Err() != nil { + return nil, nil, ctx.Err() + } + } +} diff --git a/protocol/receipt/server.go b/protocol/receipt/server.go new file mode 100644 index 00000000..b3f8fb71 --- /dev/null +++ b/protocol/receipt/server.go @@ -0,0 +1,71 @@ +package receipt + +import ( + "context" + "fmt" + "iter" + "net/http" + "strings" + + "github.com/fil-forge/ucantone/ipld/codec/dagcbor" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/ipfs/go-cid" + logging "github.com/ipfs/go-log/v2" +) + +var log = logging.Logger("receipt") + +type Finder interface { + // FindByTask returns all UCAN containers that have tokens related to the + // given task i.e. invocations or receipts for the task. + FindByTask(ctx context.Context, task cid.Cid) iter.Seq2[ucan.Container, error] +} + +// NewHandler creates a new [http.Handler] that serves receipt containers. It +// expects the URL path to end with the task CID, e.g. /receipt/ +func NewHandler(tokens Finder) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimSuffix(r.URL.Path, "/") + parts := strings.Split(path, "/") + if len(parts) == 0 { + http.Error(w, "missing task CID", http.StatusBadRequest) + return + } + task, err := cid.Parse(parts[len(parts)-1]) + if err != nil { + http.Error(w, fmt.Sprintf("invalid task CID: %v", err), http.StatusBadRequest) + return + } + + var invocations []ucan.Invocation + var delegations []ucan.Delegation + var receipts []ucan.Receipt + for c, err := range tokens.FindByTask(r.Context(), task) { + if err != nil { + http.Error(w, fmt.Sprintf("failed to find receipt token: %v", err), http.StatusInternalServerError) + return + } + invocations = append(invocations, c.Invocations()...) + delegations = append(delegations, c.Delegations()...) + receipts = append(receipts, c.Receipts()...) + } + + out := container.New( + container.WithInvocations(invocations...), + container.WithDelegations(delegations...), + container.WithReceipts(receipts...), + ) + + if _, ok := out.Receipt(task); !ok { + http.Error(w, "receipt not found for task", http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", dagcbor.ContentType) + err = out.MarshalCBOR(w) + if err != nil { + log.Errorw("marshaling receipt container", "error", err) + } + }) +} diff --git a/protocol/retrieval/client.go b/protocol/retrieval/client.go new file mode 100644 index 00000000..6c11e6cc --- /dev/null +++ b/protocol/retrieval/client.go @@ -0,0 +1,88 @@ +package retrieval + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/fil-forge/ucantone/client" + "github.com/fil-forge/ucantone/execution" +) + +type HTTPHeaderClient struct { + *client.Client[*http.Request, *http.Response] +} + +type httpHeaderClientConfig struct { + client *http.Client + headers http.Header + listeners []client.EventListener +} + +type ClientOption func(*httpHeaderClientConfig) error + +func WithHTTPClient(client *http.Client) ClientOption { + return func(cfg *httpHeaderClientConfig) error { + cfg.client = client + return nil + } +} + +// WithHTTPHeaders adds custom HTTP headers to EVERY request. Note that the +// "X-UCAN-Container" header is reserved for encoding the UCAN invocation and +// should not be included in the provided headers. +func WithHTTPHeaders(headers http.Header) ClientOption { + return func(cfg *httpHeaderClientConfig) error { + if headers.Get(HTTPHeaderName) != "" { + return fmt.Errorf("cannot set %q header with WithHTTPHeaders, it is reserved for encoding the UCAN invocation", HTTPHeaderName) + } + cfg.headers = headers + return nil + } +} + +// WithEventListener adds an event listener to the HTTP client for monitoring +// requests and responses. +func WithEventListener(listener client.EventListener) ClientOption { + return func(cfg *httpHeaderClientConfig) error { + cfg.listeners = append(cfg.listeners, listener) + return nil + } +} + +func NewClient(serviceURL *url.URL, options ...ClientOption) (*HTTPHeaderClient, error) { + codec := DefaultHTTPHeaderOutboundCodec + cfg := httpHeaderClientConfig{ + client: http.DefaultClient, + } + for _, opt := range options { + if err := opt(&cfg); err != nil { + return nil, err + } + } + c := client.New(&httpTransport{cfg.client, cfg.headers, serviceURL}, codec) + c.Listeners = cfg.listeners + return &HTTPHeaderClient{Client: c}, nil +} + +// Note: execution response metadata is a [HTTPHeaderResponseContainer] and it +// is the caller's responsibility to close the response body. +func (c *HTTPHeaderClient) Execute(execRequest execution.Request) (execution.Response, error) { + return c.Client.Execute(execRequest) +} + +type httpTransport struct { + client *http.Client + headers http.Header + url *url.URL +} + +func (t *httpTransport) RoundTrip(r *http.Request) (*http.Response, error) { + r.URL = t.url + for key, values := range t.headers { + for _, value := range values { + r.Header.Add(key, value) + } + } + return t.client.Do(r) +} diff --git a/protocol/retrieval/client_test.go b/protocol/retrieval/client_test.go new file mode 100644 index 00000000..feed06eb --- /dev/null +++ b/protocol/retrieval/client_test.go @@ -0,0 +1,201 @@ +package retrieval_test + +import ( + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/ipld/datamodel" + "github.com/fil-forge/ucantone/testutil" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/stretchr/testify/require" + + "github.com/fil-forge/forge/protocol/retrieval" +) + +// startTestServer spins up a retrieval server that registers the given +// handler for `/content/retrieve` and returns its base URL plus the service +// issuer. +func startTestServer(t *testing.T, handler execution.HandlerFunc) (*url.URL, ucan.Issuer) { + t.Helper() + service := testutil.RandomIssuer(t) + s := retrieval.NewServer(service) + s.Handle(contentRetrieve.Command, handler) + httpServer := httptest.NewServer(s) + t.Cleanup(httpServer.Close) + u, err := url.Parse(httpServer.URL) + require.NoError(t, err) + return u, service +} + +func TestClient(t *testing.T) { + t.Run("execute round trip", func(t *testing.T) { + alice := testutil.RandomIssuer(t) + blobBytes := []byte("retrieved blob bytes") + + serviceURL, service := startTestServer(t, func(req execution.Request, res execution.Response) error { + require.NoError(t, res.SetMetadata(&retrieval.HTTPHeaderResponseContainer{ + Container: container.New(), + StatusCode: http.StatusOK, + Header: http.Header{ + "Content-Type": []string{"application/octet-stream"}, + }, + Body: io.NopCloser(bytes.NewReader(blobBytes)), + })) + return res.SetSuccess(datamodel.Map{}) + }) + + client, err := retrieval.NewClient(serviceURL) + require.NoError(t, err) + + inv, err := contentRetrieve.Invoke( + alice, + alice.DID(), + &datamodel.Map{}, + invocation.WithAudience(service.DID()), + ) + require.NoError(t, err) + + res, err := client.Execute(execution.NewRequest(context.Background(), inv)) + require.NoError(t, err) + + // The receipt for the executed task should be a success. + require.NotNil(t, res.Receipt()) + require.Equal(t, inv.Task().Link(), res.Receipt().Ran()) + _, x := res.Receipt().Out().Unpack() + require.Nil(t, x) + + // The response metadata is a *HTTPHeaderResponseContainer carrying the + // retrieved blob bytes — the caller is responsible for closing the body. + hcRes, ok := res.Metadata().(*retrieval.HTTPHeaderResponseContainer) + require.True(t, ok, "expected response metadata to be a *HTTPHeaderResponseContainer") + require.Equal(t, http.StatusOK, hcRes.StatusCode) + require.Equal(t, "application/octet-stream", hcRes.Header.Get("Content-Type")) + + body, err := io.ReadAll(hcRes.Body) + require.NoError(t, err) + require.NoError(t, hcRes.Body.Close()) + require.Equal(t, blobBytes, body) + }) + + t.Run("with HTTP headers adds headers to every request", func(t *testing.T) { + alice := testutil.RandomIssuer(t) + const headerName = "X-Test-Auth" + const headerValue = "token-123" + + var seen []string + serviceURL, service := startTestServer(t, func(req execution.Request, res execution.Response) error { + hcReq, ok := req.Metadata().(*retrieval.HTTPHeaderRequestContainer) + require.True(t, ok) + seen = append(seen, hcReq.Header.Get(headerName)) + return res.SetSuccess(datamodel.Map{}) + }) + + client, err := retrieval.NewClient(serviceURL, retrieval.WithHTTPHeaders(http.Header{ + headerName: []string{headerValue}, + })) + require.NoError(t, err) + + for range 2 { + inv, err := contentRetrieve.Invoke( + alice, + alice.DID(), + &datamodel.Map{}, + invocation.WithAudience(service.DID()), + ) + require.NoError(t, err) + _, err = client.Execute(execution.NewRequest(context.Background(), inv)) + require.NoError(t, err) + } + + require.Equal(t, []string{headerValue, headerValue}, seen) + }) + + t.Run("with event listener observes request and response", func(t *testing.T) { + alice := testutil.RandomIssuer(t) + + serviceURL, service := startTestServer(t, func(req execution.Request, res execution.Response) error { + return res.SetSuccess(datamodel.Map{}) + }) + + listener := &recordingListener{} + client, err := retrieval.NewClient(serviceURL, retrieval.WithEventListener(listener)) + require.NoError(t, err) + + inv, err := contentRetrieve.Invoke( + alice, + alice.DID(), + &datamodel.Map{}, + invocation.WithAudience(service.DID()), + ) + require.NoError(t, err) + + _, err = client.Execute(execution.NewRequest(context.Background(), inv)) + require.NoError(t, err) + + require.NotNil(t, listener.encoded, "OnRequestEncode should have been called") + require.Len(t, listener.encoded.Invocations(), 1) + require.Equal(t, inv.Task().Link(), listener.encoded.Invocations()[0].Task().Link()) + + require.NotNil(t, listener.decoded, "OnResponseDecode should have been called") + require.Len(t, listener.decoded.Receipts(), 1) + require.Equal(t, inv.Task().Link(), listener.decoded.Receipts()[0].Ran()) + }) + + t.Run("with HTTP client uses provided client", func(t *testing.T) { + alice := testutil.RandomIssuer(t) + + serviceURL, service := startTestServer(t, func(req execution.Request, res execution.Response) error { + return res.SetSuccess(datamodel.Map{}) + }) + + httpClient := &http.Client{Transport: &countingTransport{inner: http.DefaultTransport}} + client, err := retrieval.NewClient(serviceURL, retrieval.WithHTTPClient(httpClient)) + require.NoError(t, err) + + inv, err := contentRetrieve.Invoke( + alice, + alice.DID(), + &datamodel.Map{}, + invocation.WithAudience(service.DID()), + ) + require.NoError(t, err) + + _, err = client.Execute(execution.NewRequest(context.Background(), inv)) + require.NoError(t, err) + + require.Equal(t, 1, httpClient.Transport.(*countingTransport).count) + }) +} + +type recordingListener struct { + encoded ucan.Container + decoded ucan.Container +} + +func (l *recordingListener) OnRequestEncode(_ context.Context, ct ucan.Container) error { + l.encoded = ct + return nil +} + +func (l *recordingListener) OnResponseDecode(_ context.Context, ct ucan.Container) error { + l.decoded = ct + return nil +} + +type countingTransport struct { + inner http.RoundTripper + count int +} + +func (t *countingTransport) RoundTrip(r *http.Request) (*http.Response, error) { + t.count++ + return t.inner.RoundTrip(r) +} diff --git a/protocol/retrieval/server.go b/protocol/retrieval/server.go new file mode 100644 index 00000000..cede86fd --- /dev/null +++ b/protocol/retrieval/server.go @@ -0,0 +1,135 @@ +package retrieval + +import ( + "context" + "fmt" + "io" + "net/http" + + "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/server" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" +) + +// Server is a UCAN server that accepts a single executable invocation +// carried in the "X-UCAN-Container" HTTP header, leaving the request and +// response bodies free to transport raw bytes for the requested blob. +// +// The original HTTP request is exposed to handlers as a +// [*HTTPHeaderRequestContainer] on the execution request metadata, and a +// handler may set a [*HTTPHeaderResponseContainer] as its response metadata +// to control the HTTP response status code, headers and body. +// +// Since the request and response bodies can only service a single retrieval, +// the request container MUST contain exactly one invocation addressed to +// this server (additional invocations not addressed to the server are +// permitted and ignored). +type Server struct { + *server.HTTPServer + id ucan.Issuer + codec *HTTPHeaderInboundCodec +} + +// NewServer creates a new UCAN retrieval server. +func NewServer(id ucan.Issuer, options ...server.HTTPOption) *Server { + codec := DefaultHTTPHeaderInboundCodec + options = append(options, server.WithHTTPCodec(codec)) + return &Server{ + HTTPServer: server.NewHTTP(id, options...), + id: id, + codec: codec, + } +} + +// ServeHTTP implements [http.Handler]. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + resp, err := s.RoundTrip(r) + if err != nil { + http.Error(w, fmt.Sprintf("handling request: %v", err), http.StatusInternalServerError) + return + } + for k, vv := range resp.Header { + for _, v := range vv { + w.Header().Add(k, v) + } + } + w.WriteHeader(resp.StatusCode) + if resp.Body != nil { + io.Copy(w, resp.Body) + resp.Body.Close() + } +} + +// RoundTrip unpacks and executes an incoming request, returning the response. +func (s *Server) RoundTrip(r *http.Request) (*http.Response, error) { + reqContainer, err := s.codec.Decode(r) + if err != nil { + return nil, fmt.Errorf("decoding request: %w", err) + } + + var inv ucan.Invocation + for _, candidate := range reqContainer.Invocations() { + aud := candidate.Audience() + if !aud.Defined() { + aud = candidate.Subject() + } + if aud != s.id.DID() { + continue + } + if inv != nil { + return nil, fmt.Errorf("expected exactly 1 invocation addressed to the server, found multiple") + } + inv = candidate + } + if inv == nil { + return nil, fmt.Errorf("missing UCAN invocation") + } + + res, err := s.Execute(&request{r.Context(), inv, reqContainer}) + if err != nil { + return nil, fmt.Errorf("executing task %s: %w", inv.Task().Link(), err) + } + + receipts := []ucan.Receipt{res.Receipt()} + var invocations []ucan.Invocation + var delegations []ucan.Delegation + var httpMeta *HTTPHeaderResponseContainer + if res.Metadata() != nil { + invocations = append(invocations, res.Metadata().Invocations()...) + delegations = append(delegations, res.Metadata().Delegations()...) + receipts = append(receipts, res.Metadata().Receipts()...) + httpMeta, _ = res.Metadata().(*HTTPHeaderResponseContainer) + } + + respContainer := container.New( + container.WithInvocations(invocations...), + container.WithDelegations(delegations...), + container.WithReceipts(receipts...), + ) + + if httpMeta != nil { + return s.codec.Encode(&HTTPHeaderResponseContainer{ + Container: respContainer, + StatusCode: httpMeta.StatusCode, + Header: httpMeta.Header, + Body: httpMeta.Body, + }) + } + + return s.codec.Encode(respContainer) +} + +// request implements [execution.Request] and lets us pass the +// [HTTPHeaderRequestContainer] through to handlers as request metadata. +type request struct { + ctx context.Context + invocation ucan.Invocation + metadata ucan.Container +} + +var _ execution.Request = (*request)(nil) + +func (r *request) Context() context.Context { return r.ctx } +func (r *request) Invocation() ucan.Invocation { return r.invocation } +func (r *request) Metadata() ucan.Container { return r.metadata } diff --git a/protocol/retrieval/server_test.go b/protocol/retrieval/server_test.go new file mode 100644 index 00000000..8916476e --- /dev/null +++ b/protocol/retrieval/server_test.go @@ -0,0 +1,110 @@ +package retrieval_test + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/execution" + "github.com/fil-forge/ucantone/ipld/datamodel" + "github.com/fil-forge/ucantone/testutil" + "github.com/fil-forge/ucantone/ucan/command" + "github.com/fil-forge/ucantone/ucan/container" + "github.com/fil-forge/ucantone/ucan/invocation" + "github.com/stretchr/testify/require" + + "github.com/fil-forge/forge/protocol/retrieval" +) + +var contentRetrieve = binding.Bind[*datamodel.Map, *datamodel.Map](command.MustParse("/content/retrieve")) + +func TestServer(t *testing.T) { + service := testutil.RandomIssuer(t) + alice := testutil.RandomIssuer(t) + + blobBytes := []byte("retrieved blob bytes") + const customHeader = "X-Test-Custom" + const customHeaderValue = "hello-world" + + var ( + capturedMethod string + capturedURL *url.URL + capturedHeader http.Header + ) + + s := retrieval.NewServer(service) + s.Handle(contentRetrieve.Command, func(req execution.Request, res execution.Response) error { + hcReq, ok := req.Metadata().(*retrieval.HTTPHeaderRequestContainer) + require.True(t, ok, "expected HTTPHeaderRequestContainer as request metadata") + capturedMethod = hcReq.Method + capturedURL = hcReq.URL + capturedHeader = hcReq.Header + + respMeta := &retrieval.HTTPHeaderResponseContainer{ + Container: container.New(), + StatusCode: http.StatusOK, + Header: http.Header{ + "Content-Type": []string{"application/octet-stream"}, + }, + Body: io.NopCloser(bytes.NewReader(blobBytes)), + } + if err := res.SetMetadata(respMeta); err != nil { + return err + } + return res.SetSuccess(datamodel.Map{}) + }) + + httpServer := httptest.NewServer(s) + t.Cleanup(httpServer.Close) + + inv, err := contentRetrieve.Invoke( + alice, + alice.DID(), + &datamodel.Map{}, + invocation.WithAudience(service.DID()), + ) + require.NoError(t, err) + + reqContainer := container.New(container.WithInvocations(inv)) + ctBytes, err := container.Encode(container.Base64Gzip, reqContainer) + require.NoError(t, err) + + httpReq, err := http.NewRequest(http.MethodGet, httpServer.URL+"/blob/abc123", nil) + require.NoError(t, err) + httpReq.Header.Set(retrieval.HTTPHeaderName, string(ctBytes)) + httpReq.Header.Set(customHeader, customHeaderValue) + + httpResp, err := http.DefaultClient.Do(httpReq) + require.NoError(t, err) + t.Cleanup(func() { httpResp.Body.Close() }) + + body, err := io.ReadAll(httpResp.Body) + require.NoError(t, err) + + // The response body should carry the retrieved blob bytes. + require.Equal(t, http.StatusOK, httpResp.StatusCode) + require.Equal(t, "application/octet-stream", httpResp.Header.Get("Content-Type")) + require.Equal(t, blobBytes, body) + + // The handler should have observed the HTTP request properties. + require.Equal(t, http.MethodGet, capturedMethod) + require.NotNil(t, capturedURL) + require.Equal(t, "/blob/abc123", capturedURL.Path) + require.Equal(t, customHeaderValue, capturedHeader.Get(customHeader)) + require.Equal(t, string(ctBytes), capturedHeader.Get(retrieval.HTTPHeaderName)) + + // The UCAN response container travels in the X-UCAN-Container header. + respHeaderCT := httpResp.Header.Get(retrieval.HTTPHeaderName) + require.NotEmpty(t, respHeaderCT) + require.Contains(t, httpResp.Header.Values("Vary"), retrieval.HTTPHeaderName) + + respContainer, err := container.Decode([]byte(respHeaderCT)) + require.NoError(t, err) + require.Len(t, respContainer.Receipts(), 1) + _, x := respContainer.Receipts()[0].Out().Unpack() + require.Nil(t, x) +} diff --git a/protocol/retrieval/transport.go b/protocol/retrieval/transport.go new file mode 100644 index 00000000..bd90fbd4 --- /dev/null +++ b/protocol/retrieval/transport.go @@ -0,0 +1,135 @@ +package retrieval + +import ( + "fmt" + "io" + "net/http" + "net/url" + + "github.com/fil-forge/ucantone/transport" + "github.com/fil-forge/ucantone/ucan" + "github.com/fil-forge/ucantone/ucan/container" +) + +var ( + DefaultHTTPHeaderInboundCodec = &HTTPHeaderInboundCodec{} + DefaultHTTPHeaderOutboundCodec = &HTTPHeaderOutboundCodec{} + HTTPHeaderName = "X-UCAN-Container" +) + +type HTTPHeaderRequestContainer struct { + ucan.Container + Method string + URL *url.URL + Header http.Header + Body io.ReadCloser +} + +type HTTPHeaderResponseContainer struct { + ucan.Container + StatusCode int + Header http.Header + Body io.ReadCloser +} + +type HTTPHeaderInboundCodec struct{} + +var _ transport.InboundCodec[*http.Request, *http.Response] = (*HTTPHeaderInboundCodec)(nil) + +func (h *HTTPHeaderInboundCodec) Decode(r *http.Request) (ucan.Container, error) { + if r.Header.Get(HTTPHeaderName) == "" { + return nil, fmt.Errorf("missing required %q header", HTTPHeaderName) + } + ct, err := container.Decode([]byte(r.Header.Get(HTTPHeaderName))) + if err != nil { + return nil, fmt.Errorf("decoding container: %w", err) + } + return &HTTPHeaderRequestContainer{ + Container: ct, + Method: r.Method, + URL: r.URL, + Header: r.Header, + Body: r.Body, + }, nil +} + +func (h *HTTPHeaderInboundCodec) Encode(c ucan.Container) (*http.Response, error) { + status := http.StatusOK + headers := http.Header{} + var body io.ReadCloser + if hc, ok := c.(*HTTPHeaderResponseContainer); ok { + if hc.StatusCode != 0 { + status = hc.StatusCode + } + if hc.Header != nil { + headers = hc.Header + } + body = hc.Body + } + resp := &http.Response{ + StatusCode: status, + Header: headers, + Body: body, + } + ctBytes, err := container.Encode(container.Base64Gzip, c) + if err != nil { + return nil, fmt.Errorf("encoding container: %w", err) + } + resp.Header.Set(HTTPHeaderName, string(ctBytes)) + // ensure the Vary header is set for ALL responses + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary + resp.Header.Add("Vary", HTTPHeaderName) + return resp, nil +} + +type HTTPResponseContainer struct { + ucan.Container + Response *http.Response +} + +type HTTPHeaderOutboundCodec struct{} + +var _ transport.OutboundCodec[*http.Request, *http.Response] = (*HTTPHeaderOutboundCodec)(nil) + +func (h *HTTPHeaderOutboundCodec) Encode(c ucan.Container) (*http.Request, error) { + method := http.MethodGet + headers := http.Header{} + var body io.ReadCloser + if hc, ok := c.(*HTTPHeaderRequestContainer); ok { + if hc.Method != "" { + method = hc.Method + } + if hc.Header != nil { + headers = hc.Header + } + body = hc.Body + } + req := &http.Request{ + Method: method, + Body: body, + Header: headers, + } + ctBytes, err := container.Encode(container.Base64Gzip, c) + if err != nil { + return nil, fmt.Errorf("encoding container: %w", err) + } + req.Header.Set(HTTPHeaderName, string(ctBytes)) + return req, nil +} + +func (h *HTTPHeaderOutboundCodec) Decode(r *http.Response) (ucan.Container, error) { + if r.Header.Get(HTTPHeaderName) == "" { + return nil, fmt.Errorf("missing required %q header", HTTPHeaderName) + } + ct, err := container.Decode([]byte(r.Header.Get(HTTPHeaderName))) + if err != nil { + return nil, fmt.Errorf("decoding container: %w", err) + } + hct := HTTPHeaderResponseContainer{ + Container: ct, + StatusCode: r.StatusCode, + Header: r.Header, + Body: r.Body, + } + return &hct, nil +} diff --git a/smelt/CLAUDE.md b/smelt/CLAUDE.md index 1b4e1246..749b3bd5 100644 --- a/smelt/CLAUDE.md +++ b/smelt/CLAUDE.md @@ -461,7 +461,7 @@ Available variables: `PIRI_IMAGE`, `GUPPY_IMAGE`, `DELEGATOR_IMAGE`, `INDEXER_IM ## Developing Against Sibling Service Repos -To work on a feature/bugfix spanning the service repos (and the shared `libforge` library) +To work on a feature/bugfix spanning the service repos (and the shared `protocol`, `internal` and `attestation` modules) and validate it in smelt, use a **Go workspace** (`go.work`) plus `SMELT_WORKSPACE=1`: smelt compiles the services you're editing from local source and bind-mounts the binaries over the otherwise-published images — no Dockerfiles, no image rebuilds. **Full walkthrough: @@ -470,9 +470,9 @@ otherwise-published images — no Dockerfiles, no image rebuilds. **Full walkthr - `go.work` lives at the `fil-forge/` parent (above every repo, gitignored) and lists `smelt` plus the repos you're editing — `go work init ./smelt ./piri`. The `use`-list is the single source of truth for what gets rebuilt. -- **libforge rule:** a service is rebuilt when its module is in the use-list; listing - `libforge` forces **all** services to rebuild (a published binary would still link the - published libforge). +- **shared-module rule:** a service is rebuilt when its module is in the use-list; listing + `protocol`, `internal` or `attestation` forces **all** services to rebuild (a published binary would still link the + published copy). - Run `SMELT_WORKSPACE=1 make up` / `make fresh`, or `SMELT_WORKSPACE=1 go test -tags e2e ./tests/e2e`. The flag runs `smelt workspace build` → binaries in `generated/bin/` + mounts in `generated/compose/workspace.override.yml` (chained into `$(COMPOSE)`); a plain `make up` diff --git a/smelt/README.md b/smelt/README.md index 3c3fc338..d34264ad 100644 --- a/smelt/README.md +++ b/smelt/README.md @@ -181,7 +181,7 @@ gotchas — lives in [docs/SNAPSHOTS.md](docs/SNAPSHOTS.md). - **[Multi-Piri Configuration](docs/MULTI_PIRI.md)** — Running multiple piri nodes via `smelt.yml` - **[Snapshots](docs/SNAPSHOTS.md)** — Capture and restore stack state to skip cold-boot time - **[Architecture Guide](docs/ARCHITECTURE.md)** — How the services connect and why -- **[Developing Across Service Repos](docs/DEVELOPING.md)** — Run local changes to piri/sprue/libforge/etc. in the stack via `go.work` +- **[Developing Across Service Repos](docs/DEVELOPING.md)** — Run local changes to piri/sprue/protocol/etc. in the stack via `go.work` - **[Troubleshooting](docs/TROUBLESHOOTING.md)** — When things go wrong (they will) - **[Extending Smelt](docs/EXTENDING.md)** — Adding services or modifying the environment diff --git a/smelt/cmd/smelt/cmd/workspace.go b/smelt/cmd/smelt/cmd/workspace.go index 0469011c..ac1eff87 100644 --- a/smelt/cmd/smelt/cmd/workspace.go +++ b/smelt/cmd/smelt/cmd/workspace.go @@ -23,8 +23,9 @@ var workspaceBuildCmd = &cobra.Command{ Short: "Compile workspace-selected service binaries and write the mount override", Long: `Reads the active go.work use-list, compiles each selected service from its sibling checkout into generated/bin/, and writes generated/compose/workspace.override.yml -mounting each binary over the published image. If libforge is in the workspace, -every service is rebuilt (a published binary would still link published libforge).`, +mounting each binary over the published image. If a shared module (protocol, internal, +attestation) is in the workspace, every service is rebuilt (a published binary would +still link the published copy).`, RunE: runWorkspaceBuild, } diff --git a/smelt/docs/DEVELOPING.md b/smelt/docs/DEVELOPING.md index 0bca78c4..17e06be0 100644 --- a/smelt/docs/DEVELOPING.md +++ b/smelt/docs/DEVELOPING.md @@ -1,7 +1,7 @@ # Developing Against Sibling Service Repos Smelt normally runs **published** images for every service. When you're changing the code -of a service — or the shared `libforge` library — and want to validate it in the full stack, +of a service — or the shared `protocol`/`internal`/`attestation` modules — and want to validate it in the full stack, use a **Go workspace** (`go.work`) plus the `SMELT_WORKSPACE=1` flag. ## What this gives you @@ -10,7 +10,7 @@ With the flag set, smelt: 1. reads the active `go.work` to decide which services you're editing, 2. compiles each from your local checkout into a static `linux/amd64` binary (the workspace - bakes in your cross-module edits, including a local `libforge`), and + bakes in your cross-module edits, including local edits to the shared modules), and 3. bind-mounts each binary over the binary in the otherwise-**published** image. So the published image still provides the runtime (base OS, certs, side tools like guppy's @@ -28,7 +28,7 @@ it from any of them. It must list `smelt` plus the repos you're editing: ~/workspace/src/github.com/fil-forge/ ├── go.work # the workspace file (local-only; see below) ├── smelt/ # this repo -├── libforge/ # shared library +├── protocol/, internal/, attestation/ # shared modules ├── piri/ # piri storage node ├── sprue/ # upload service ├── indexing-service/ # indexer @@ -80,17 +80,17 @@ siblings (piri-pdp, sprue, …) don't need it. > replace above) only when you want `SMELT_WORKSPACE=1` smelt runs to rebuild ingot from local > source. -### Selection and the `libforge` rule +### Selection and the shared-module rule A service is rebuilt from local source when its module dir is in the `use`-list. Because the -siblings resolve `libforge` *only* through the workspace, **listing `libforge` forces all six -services to rebuild** — a published binary would otherwise still link the published `libforge`. +every service compiles the shared modules in, **listing any of `protocol`, `internal` or `attestation` forces all +services to rebuild** — a published binary would otherwise still link the published copy. | Editing… | put in `go.work` | smelt rebuilds | |---|---|---| | just piri | `./smelt ./piri` | piri | | upload + indexer | `./smelt ./sprue ./indexing-service` | upload, indexer | -| the shared lib | `./smelt ./libforge` | **all six services** | +| a shared module | `./smelt ./protocol` | **all six services** | Module → service → container binary (defined in `pkg/workspace`): diff --git a/smelt/docs/EXTENDING.md b/smelt/docs/EXTENDING.md index 8dc745b4..1a9b8bdd 100644 --- a/smelt/docs/EXTENDING.md +++ b/smelt/docs/EXTENDING.md @@ -404,7 +404,7 @@ docker push ghcr.io/yourorg/upload:dev ### Using Local Builds of Service Repositories -To develop a service (or the shared `libforge` library) against the full stack, use a Go +To develop a service (or the shared `protocol`/`internal`/`attestation` modules) against the full stack, use a Go workspace plus `SMELT_WORKSPACE=1`. Smelt compiles your local checkouts into static binaries and bind-mounts them over the published images — no image rebuilds, no per-repo Dockerfiles. diff --git a/smelt/embed.go b/smelt/embed.go index a13a2dc5..734ccef8 100644 --- a/smelt/embed.go +++ b/smelt/embed.go @@ -4,7 +4,7 @@ package smelt import "embed" // EmbeddedFiles contains all compose files, configs, and scripts needed to run the stack. -// Keys and proofs are generated at runtime using ucantone + libforge. +// Keys and proofs are generated at runtime using ucantone + the protocol module. // //go:embed compose.yml .env diff --git a/smelt/go.mod b/smelt/go.mod index 2c24fec5..d94fcbdf 100644 --- a/smelt/go.mod +++ b/smelt/go.mod @@ -1,6 +1,6 @@ module github.com/fil-forge/forge/smelt -go 1.25.9 +go 1.27.0 require ( github.com/aws/aws-sdk-go-v2 v1.42.0 @@ -8,11 +8,14 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.19.25 github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1 github.com/docker/docker v28.5.2+incompatible - github.com/fil-forge/libforge v0.0.0-20260731172903-928cf2a21b7e - github.com/fil-forge/ucantone v0.0.0-20260727203046-ccb77059de44 + github.com/fil-forge/forge/internal v0.0.0 + github.com/fil-forge/forge/protocol v0.0.0 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 + github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a github.com/filecoin-project/go-fee v0.1.1-0.20260826122013-1c3970b90b0d github.com/google/uuid v1.6.0 github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f + github.com/multiformats/go-multihash v0.2.3 github.com/spf13/cobra v1.10.2 github.com/testcontainers/testcontainers-go v0.44.0 github.com/testcontainers/testcontainers-go/modules/compose v0.44.0 @@ -21,6 +24,12 @@ require ( ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/DefangLabs/secret-detector v0.0.0-20250811234530-d4b4214cd679 // indirect + github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/alanshaw/dag-json-gen v0.0.9 // indirect github.com/andybalholm/brotli v1.2.2 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect @@ -37,27 +46,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.43.4 // indirect github.com/aws/smithy-go v1.27.3 // indirect - github.com/fxamacker/cbor/v2 v2.9.2 // indirect - github.com/gofiber/fiber/v3 v3.3.0 // indirect - github.com/gofiber/schema v1.8.0 // indirect - github.com/gofiber/utils/v2 v2.1.1 // indirect - github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect - github.com/philhofer/fwd v1.2.0 // indirect - github.com/tinylib/msgp v1.6.4 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.72.0 // indirect - github.com/x448/float16 v0.8.4 // indirect - github.com/zeebo/xxh3 v1.1.0 // indirect -) - -require ( - dario.cat/mergo v1.0.2 // indirect - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/DefangLabs/secret-detector v0.0.0-20250811234530-d4b4214cd679 // indirect - github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect - github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/alanshaw/dag-json-gen v0.0.8 // indirect github.com/buger/goterm v1.0.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect @@ -75,7 +63,6 @@ require ( github.com/containerd/ttrpc v1.2.8 // indirect github.com/containerd/typeurl/v2 v2.3.0 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/buildx v0.35.0 // indirect github.com/docker/cli v29.6.0+incompatible // indirect @@ -86,15 +73,18 @@ require ( github.com/ebitengine/purego v0.10.1 // indirect github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect - github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a github.com/filecoin-project/go-data-segment v0.0.1 // indirect github.com/fsnotify/fsevents v0.2.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/gofiber/fiber/v3 v3.3.0 // indirect + github.com/gofiber/schema v1.8.0 // indirect + github.com/gofiber/utils/v2 v2.1.1 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -108,12 +98,14 @@ require ( github.com/in-toto/in-toto-golang v0.11.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect - github.com/ipfs/go-cid v0.6.1 // indirect + github.com/ipfs/go-cid v0.6.2 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/klauspost/compress v1.18.6 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect github.com/mattn/go-shellwords v1.0.13 // indirect github.com/minio/sha256-simd v1.0.1 // indirect @@ -139,14 +131,13 @@ require ( github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multibase v0.3.0 // indirect github.com/multiformats/go-multicodec v0.10.0 // indirect - github.com/multiformats/go-multihash v0.2.3 github.com/multiformats/go-varint v0.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.4.1 // indirect + github.com/philhofer/fwd v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect @@ -158,8 +149,9 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/testify v1.11.1 // indirect + github.com/stretchr/testify v1.12.1 // indirect github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect + github.com/tinylib/msgp v1.6.4 // indirect github.com/tklauser/go-sysconf v0.4.0 // indirect github.com/tklauser/numcpus v0.12.0 // indirect github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect @@ -167,10 +159,14 @@ require ( github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.72.0 // indirect github.com/whyrusleeping/cbor-gen v0.3.1 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 // indirect @@ -186,14 +182,14 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect + golang.org/x/net v0.57.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d // indirect @@ -206,3 +202,7 @@ require ( pitr.ca/jsontokenizer v0.3.2 // indirect tags.cncf.io/container-device-interface v1.1.0 // indirect ) + +replace github.com/fil-forge/forge/protocol => ../protocol + +replace github.com/fil-forge/forge/internal => ../internal diff --git a/smelt/go.sum b/smelt/go.sum index 2e95fbf9..a96cb462 100644 --- a/smelt/go.sum +++ b/smelt/go.sum @@ -16,8 +16,8 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/alanshaw/dag-json-gen v0.0.8 h1:Y0SfO2bp9ECDvcNbw6aQ91jmnfLC7UgRDshCeASDfT8= -github.com/alanshaw/dag-json-gen v0.0.8/go.mod h1:v1YBZcS4B355MqxtyQr+fGNbEhm0CzHd+gOqOO/MZ+I= +github.com/alanshaw/dag-json-gen v0.0.9 h1:q59Ra1mQB6HnFGI1ilFxsZvF1CqGN0XDlUWq16EJ6pw= +github.com/alanshaw/dag-json-gen v0.0.9/go.mod h1:v1YBZcS4B355MqxtyQr+fGNbEhm0CzHd+gOqOO/MZ+I= github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30= github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= @@ -124,8 +124,6 @@ github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= @@ -158,10 +156,8 @@ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJ github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/fil-forge/libforge v0.0.0-20260731172903-928cf2a21b7e h1:IrmG+IH92M3H3pMYvpVn0BLefpS0y65cWq5eJzMrqo8= -github.com/fil-forge/libforge v0.0.0-20260731172903-928cf2a21b7e/go.mod h1:zttbhcjvrPuykvZfl+s7q4E+7FN+uFoAvKeipXryuOA= -github.com/fil-forge/ucantone v0.0.0-20260727203046-ccb77059de44 h1:ofvb2Qq7++VPRelGsLbtnd1ZMKVT4n4QGoa79BhJ6VQ= -github.com/fil-forge/ucantone v0.0.0-20260727203046-ccb77059de44/go.mod h1:oFY5BfD0bDeodGlbBHh3/nK99MAS93rGXjoQz7s5qgE= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a h1:lDwnNmF4LNbevx/YYNCC0CazMiL4eIe38MvfqbRR3RA= github.com/fil-forge/versitygw v0.0.0-20260716095011-7a65883d595a/go.mod h1:t73Wa2xqpT0NdY+SzRZDiO+tQD13b3UEZRoe2wYFiIE= github.com/filecoin-project/go-data-segment v0.0.1 h1:1wmDxOG4ubWQm3ZC1XI5nCon5qgSq7Ra3Rb6Dbu10Gs= @@ -277,14 +273,14 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= -github.com/ipfs/go-cid v0.6.1 h1:T5TnNb08+ueovG76Z5gx1L4Y7QOaGTXHg1F6raWFxIc= -github.com/ipfs/go-cid v0.6.1/go.mod h1:zrY0SwOhjrrIdfPQ/kf+k1sXyJ0QE7cMxfCployLBs0= +github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= +github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -299,8 +295,8 @@ github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.13 h1:DC0OMEpGjm6LfNFU4ckYcvbQKyp2vE8atyFGXNtDcf4= @@ -442,8 +438,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV 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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/testcontainers/testcontainers-go v0.44.0 h1:/Fwh6HY1mIikhnm9e7HwoxGycx0lzRAE0f5VQpjFxzI= github.com/testcontainers/testcontainers-go v0.44.0/go.mod h1:IcnwQrYTO86xHXu5bvMaBH7ATlbS3Qn1M1QWW3c66rE= github.com/testcontainers/testcontainers-go/modules/compose v0.44.0 h1:8YcW51jhgpkkiRVe10Wj9TCBthJmoNpU2fK5WSf7TQ8= @@ -539,18 +536,19 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q= -golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -563,8 +561,8 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= diff --git a/smelt/pkg/generate/keys.go b/smelt/pkg/generate/keys.go index b0f4e835..1675ed4a 100644 --- a/smelt/pkg/generate/keys.go +++ b/smelt/pkg/generate/keys.go @@ -12,7 +12,7 @@ import ( "path/filepath" "strings" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/smelt/pkg/manifest" "github.com/fil-forge/ucantone/multikey" ) diff --git a/smelt/pkg/stack/build.go b/smelt/pkg/stack/build.go index c8437271..08a5f958 100644 --- a/smelt/pkg/stack/build.go +++ b/smelt/pkg/stack/build.go @@ -51,10 +51,11 @@ func BuildImage(t *testing.T, repoPath string, imageName string) string { } // buildForgeImage builds an in-repo forge service via the shared -// docker/Dockerfile (the monorepo has no per-service Dockerfiles): the build -// context is the service directory and the Dockerfile lives at its sibling -// ../docker/Dockerfile, parameterized by the SERVICE build arg (plus any -// extra --build-arg pairs, e.g. piri's BUILD_TAGS=skiff). +// docker/Dockerfile (the monorepo has no per-service Dockerfiles). The build +// context is the repository root — the services reach the shared modules +// through replace directives, which a per-service context could not see — and +// the Dockerfile is parameterized by the SERVICE build arg (plus any extra +// --build-arg pairs, e.g. piri's BUILD_TAGS=skiff). func buildForgeImage(t *testing.T, serviceDir, service, imageName string, extraArgs ...string) string { t.Helper() @@ -68,7 +69,7 @@ func buildForgeImage(t *testing.T, serviceDir, service, imageName string, extraA for _, a := range extraArgs { args = append(args, "--build-arg", a) } - args = append(args, serviceDir) + args = append(args, filepath.Join(serviceDir, "..")) t.Logf("Building Docker image %s for %s from %s...", tag, service, serviceDir) cmd := exec.Command("docker", args...) diff --git a/smelt/pkg/stack/options.go b/smelt/pkg/stack/options.go index 8e0204ad..9a2bde6d 100644 --- a/smelt/pkg/stack/options.go +++ b/smelt/pkg/stack/options.go @@ -186,8 +186,8 @@ func WithServiceConfig(service, path string) Option { // WithWorkspaceBinaries builds every service selected by the active Go // workspace (go.work) from local sibling source and mounts the resulting // binaries over the published images. Selection follows the use-list: a service -// is built when its module is listed; if libforge is listed, all services are -// rebuilt. Requires an active go.work (see pkg/workspace). +// is built when its module is listed; if a shared module (protocol, internal, +// attestation) is listed, all services are rebuilt. Requires an active go.work (see pkg/workspace). // // Example: // diff --git a/smelt/pkg/stack/proofs.go b/smelt/pkg/stack/proofs.go index c335270b..863f4a64 100644 --- a/smelt/pkg/stack/proofs.go +++ b/smelt/pkg/stack/proofs.go @@ -5,15 +5,15 @@ import ( "os" "path/filepath" - blobcmds "github.com/fil-forge/libforge/commands/blob" - replicacmds "github.com/fil-forge/libforge/commands/blob/replica" - "github.com/fil-forge/libforge/commands/claim" - customercmds "github.com/fil-forge/libforge/commands/customer" - pdpcmds "github.com/fil-forge/libforge/commands/pdp" - s3bkt "github.com/fil-forge/libforge/commands/s3/bucket" - s3req "github.com/fil-forge/libforge/commands/s3/request" - "github.com/fil-forge/libforge/commands/space/egress" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + replicacmds "github.com/fil-forge/forge/protocol/commands/blob/replica" + "github.com/fil-forge/forge/protocol/commands/claim" + customercmds "github.com/fil-forge/forge/protocol/commands/customer" + pdpcmds "github.com/fil-forge/forge/protocol/commands/pdp" + s3bkt "github.com/fil-forge/forge/protocol/commands/s3/bucket" + s3req "github.com/fil-forge/forge/protocol/commands/s3/request" + "github.com/fil-forge/forge/protocol/commands/space/egress" "github.com/fil-forge/forge/smelt/pkg/manifest" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/multikey" @@ -34,13 +34,13 @@ var piriCommands = []ucan.Command{ pdpcmds.Info.Command, } -// delegateFn matches the method value of a libforge bound command's Delegate +// delegateFn matches the method value of a protocol bound command's Delegate // (e.g. claim.Cache.Delegate, egress.Track.Delegate). type delegateFn func(issuer ucan.Issuer, audience, subject did.DID, opts ...delegation.Option) (ucan.Delegation, error) // generateProofs writes the UCAN delegation proofs the delegator consumes at // startup. It mirrors delegator/cmd/gen.go — the same generator the compose -// stack invokes via generate-proofs.sh — using libforge command primitives and +// stack invokes via generate-proofs.sh — using protocol command primitives and // ucantone envelope encoding, so the proofs parse with the delegator's // ucantone delegation.Decode. // @@ -159,7 +159,7 @@ func writeProofs(keysDir, proofsDir, issuerKeyName, issuerDidWeb, audienceDID, o } // writeDelegation creates one delegation from issuerKeyName's key (optionally -// wrapped as issuerDidWeb) to audienceDID, using the given libforge command, +// wrapped as issuerDidWeb) to audienceDID, using the given protocol command, // and writes the ucantone-encoded envelope to proofsDir/outputFile. func writeDelegation(keysDir, proofsDir, issuerKeyName, issuerDidWeb, audienceDID, outputFile string, delegate delegateFn) error { pemData, err := os.ReadFile(filepath.Join(keysDir, issuerKeyName+".pem")) diff --git a/smelt/pkg/workspace/workspace.go b/smelt/pkg/workspace/workspace.go index 5219def7..8990ca6f 100644 --- a/smelt/pkg/workspace/workspace.go +++ b/smelt/pkg/workspace/workspace.go @@ -6,10 +6,9 @@ // stack.WithWorkspaceBinaries() (the Go test stack). The active Go workspace // (go.work) is the single source of truth for "what am I editing": every // service whose module appears in the use-list is rebuilt from local source. -// Because the shared libforge library is resolved purely through the workspace -// (the siblings carry no replace directives), libforge appearing in the -// use-list forces every service to be rebuilt — otherwise a published binary -// would still link the published libforge pseudo-version. +// The shared modules (protocol, internal, attestation) are compiled into every +// service, so any of them appearing in the use-list forces every service to be +// rebuilt — otherwise a published binary would still link the published copy. // // Binaries are compiled on the host, where go.work resolves local cross-module // edits, then bind-mounted into otherwise-published containers. This needs no @@ -61,9 +60,10 @@ var Services = map[string]serviceBuild{ "swarf": {moduleDir: "swarf", buildTarget: "./cmd/swarf", binPath: "/usr/bin/swarf", configPath: "/etc/swarf/config.yaml"}, } -// libforgeDir is the workspace dir of the shared library. Its presence in the -// use-list forces a rebuild of every service (see package doc). -const libforgeDir = "libforge" +// sharedDirs are the workspace dirs of the modules every service compiles in. +// Any of them in the use-list forces a rebuild of every service (see package +// doc). +var sharedDirs = []string{"protocol", "internal", "attestation"} // Detect inspects the active go.work and returns the workspace root directory // (the dir containing go.work) and the sorted set of smelt services to build @@ -74,7 +74,7 @@ func Detect() (root string, services []string, err error) { return "", nil, err } if gowork == "" || gowork == "off" { - return "", nil, fmt.Errorf("no active go.work; create one at the fil-forge parent dir, e.g. `go work init ./smelt ./libforge ./piri` (see CLAUDE.md)") + return "", nil, fmt.Errorf("no active go.work; run from inside the forge checkout, whose go.work lists every module (see CLAUDE.md)") } root = filepath.Dir(gowork) @@ -87,8 +87,15 @@ func Detect() (root string, services []string, err error) { inWorkspace[filepath.Base(d)] = true } + sharedInWorkspace := false + for _, d := range sharedDirs { + if inWorkspace[d] { + sharedInWorkspace = true + } + } + selected := map[string]bool{} - if inWorkspace[libforgeDir] { + if sharedInWorkspace { for name := range Services { selected[name] = true } @@ -109,7 +116,7 @@ func Detect() (root string, services []string, err error) { // BuildBinary compiles one service's binary from its sibling module under root // into outDir, returning the absolute output path. It produces a static // linux/amd64 binary with the workspace active so local cross-module edits -// (e.g. a local libforge) are compiled in. +// (e.g. a local protocol or internal module) are compiled in. func BuildBinary(root, service, outDir string) (string, error) { spec, ok := Services[service] if !ok { diff --git a/smelt/systems/delegator/compose.yml b/smelt/systems/delegator/compose.yml index caa8d57a..c8f5743b 100644 --- a/smelt/systems/delegator/compose.yml +++ b/smelt/systems/delegator/compose.yml @@ -21,7 +21,10 @@ services: blockchain: condition: service_healthy healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/healthcheck"] + # A GET, not --spider: GNU wget (the debian service image) spiders with + # HEAD, which echo answers 405 on a GET-only route; busybox wget spiders + # with GET. -O /dev/null is a GET under both. + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:80/healthcheck"] start_interval: 1s interval: 10s timeout: 5s diff --git a/smelt/systems/indexing/indexer/compose.yml b/smelt/systems/indexing/indexer/compose.yml index 5c46fdec..b6bf23ad 100644 --- a/smelt/systems/indexing/indexer/compose.yml +++ b/smelt/systems/indexing/indexer/compose.yml @@ -43,7 +43,10 @@ services: ipni: condition: service_healthy healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"] + # A GET, not --spider (see signing-service/compose.yml): the indexer's + # mux happens to serve HEAD for GET routes, but the probe should not + # depend on the router. + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:80/"] start_interval: 1s interval: 5s timeout: 5s diff --git a/smelt/systems/signing-service/compose.yml b/smelt/systems/signing-service/compose.yml index cbdf9848..033868b8 100644 --- a/smelt/systems/signing-service/compose.yml +++ b/smelt/systems/signing-service/compose.yml @@ -26,7 +26,10 @@ services: blockchain: condition: service_healthy healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://localhost:7446/healthcheck"] + # A GET, not --spider: GNU wget (the debian service image) spiders with + # HEAD, which echo answers 405 on a GET-only route; busybox wget spiders + # with GET. -O /dev/null is a GET under both. + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:7446/healthcheck"] start_interval: 1s interval: 10s timeout: 5s diff --git a/smelt/tests/compat/compat_test.go b/smelt/tests/compat/compat_test.go index ddcf0bbe..3e89513d 100644 --- a/smelt/tests/compat/compat_test.go +++ b/smelt/tests/compat/compat_test.go @@ -187,12 +187,20 @@ func TestRollingUpgrade(t *testing.T) { } // otherThan returns every in-repo service except the named one, for use as the -// exclusion list — i.e. "build only `service` from HEAD". +// exclusion list — i.e. "build only `service` from HEAD". It takes the repo +// name the baseline map uses and returns smelt service names, which is what +// the workspace builder keys on: the two differ for sprue, whose smelt service +// is "upload". Comparing the two vocabularies directly would silently exclude +// nothing for sprue and run that case entirely on baseline images. func otherThan(service string) []string { - all := []string{"piri", "ingot", "upload", "hilt"} + smeltName := map[string]string{"piri": "piri", "ingot": "ingot", "sprue": "upload", "hilt": "hilt"} + excluded, ok := smeltName[service] + if !ok { + panic(fmt.Sprintf("otherThan: unknown service %q", service)) + } var out []string - for _, s := range all { - if s != service { + for _, repo := range []string{"piri", "ingot", "sprue", "hilt"} { + if s := smeltName[repo]; s != excluded { out = append(out, s) } } diff --git a/smelt/tests/s3/forge_multipart_deferred_test.go b/smelt/tests/s3/forge_multipart_deferred_test.go index bed03851..20e7ab7d 100644 --- a/smelt/tests/s3/forge_multipart_deferred_test.go +++ b/smelt/tests/s3/forge_multipart_deferred_test.go @@ -14,8 +14,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/forge/smelt/pkg/stack" - "github.com/fil-forge/libforge/digestutil" "github.com/multiformats/go-multihash" ) diff --git a/smelt/tests/s3/stack_test.go b/smelt/tests/s3/stack_test.go index b80ec59c..ffb7795f 100644 --- a/smelt/tests/s3/stack_test.go +++ b/smelt/tests/s3/stack_test.go @@ -25,11 +25,11 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/fil-forge/versitygw/tests/integration" + "github.com/fil-forge/forge/internal/s3perm" + s3cmd "github.com/fil-forge/forge/protocol/commands/s3" s3glue "github.com/fil-forge/forge/smelt/pkg/s3glue" "github.com/fil-forge/forge/smelt/pkg/stack" "github.com/fil-forge/forge/smelt/pkg/workspace" - s3cmd "github.com/fil-forge/libforge/commands/s3" - "github.com/fil-forge/libforge/s3perm" ) // This file is the shared harness for the integration tests: they boot the @@ -201,8 +201,8 @@ func waitHTTPOK(t *testing.T, url string, timeout time.Duration) { t.Fatalf("%s not healthy after %s", url, timeout) } -// hiltAllPermissions is every S3 permission hilt recognizes (libforge's -// s3perm map). The conformance cases exercise the whole S3 surface, so the +// hiltAllPermissions is every S3 permission hilt recognizes (internal/s3perm's +// map). The conformance cases exercise the whole S3 surface, so the // test tenant's access key carries them all. // // KEEP IN LOCKSTEP with s3perm's permission map: a permission added there but diff --git a/sprue/Makefile b/sprue/Makefile index 5bcd2ecf..3ae48521 100644 --- a/sprue/Makefile +++ b/sprue/Makefile @@ -10,7 +10,7 @@ export GOWORK := off # direct exec of a bare 'docker'. DOCKER := $(shell command -v docker) -# Container image via the monorepo-shared docker/Dockerfile (context = this dir). +# Container image via the monorepo-shared docker/Dockerfile (context = the repo root). IMAGE_BUILD_ARGS := --build-arg SERVICE=sprue .PHONY: all build test vet lint image image-dev clean gen @@ -48,10 +48,10 @@ clean: rm -f ./sprue image: - $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/sprue:local . + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target prod -t forge/sprue:local .. image-dev: - $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/sprue:local-dev . + $(DOCKER) build -f ../docker/Dockerfile $(IMAGE_BUILD_ARGS) --target dev -t forge/sprue:local-dev .. gen: go generate ./... diff --git a/sprue/cmd/client/lib/client.go b/sprue/cmd/client/lib/client.go index 42d7f755..fb1a04a9 100644 --- a/sprue/cmd/client/lib/client.go +++ b/sprue/cmd/client/lib/client.go @@ -4,7 +4,7 @@ import ( "fmt" "net/url" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/internal/config" "github.com/fil-forge/forge/sprue/internal/fx" "github.com/fil-forge/forge/sprue/pkg/client" diff --git a/sprue/go.mod b/sprue/go.mod index 8332e9ed..b22dce15 100644 --- a/sprue/go.mod +++ b/sprue/go.mod @@ -9,8 +9,10 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.19.37 github.com/aws/aws-sdk-go-v2/service/s3 v1.107.3 github.com/docker/docker v28.5.2+incompatible - github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5 - github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1 + github.com/fil-forge/forge/attestation v0.0.0 + github.com/fil-forge/forge/internal v0.0.0 + github.com/fil-forge/forge/protocol v0.0.0 + github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 github.com/google/uuid v1.6.0 github.com/ipfs/go-cid v0.6.2 github.com/jackc/pgx/v5 v5.10.0 @@ -73,13 +75,13 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/compress v1.19.1 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/labstack/gommon v0.5.0 // indirect github.com/lib/pq v1.11.1 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.15 // indirect - github.com/mattn/go-isatty v0.0.23 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/minio/minio-go/v7 v7.0.94 // indirect @@ -133,14 +135,20 @@ require ( go.uber.org/dig v1.19.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/exp v0.0.0-20260718201538-764159d718ef // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect golang.org/x/net v0.57.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260523011958-0a33c5d7ca68 // indirect lukechampine.com/blake3 v1.4.1 // indirect pitr.ca/jsontokenizer v0.3.2 // indirect ) + +replace github.com/fil-forge/forge/protocol => ../protocol + +replace github.com/fil-forge/forge/internal => ../internal + +replace github.com/fil-forge/forge/attestation => ../attestation diff --git a/sprue/go.sum b/sprue/go.sum index 299fc42b..4f8f0cce 100644 --- a/sprue/go.sum +++ b/sprue/go.sum @@ -80,10 +80,8 @@ github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/ github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5 h1:Pu7Xk/LdaQyv1560k1MbL6CQUdWxZJIG9elEE0vkgLY= -github.com/fil-forge/libforge v0.0.0-20260807225550-3e6895b41be5/go.mod h1:aL/KcXZ0KVs1ymAYqIwQyCnl2p481InN+gYbRLNM8fI= -github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1 h1:Tzy3lZ7+LAyVK4+qyWM97UMOtnXx05Wdn+rcTDLdBDE= -github.com/fil-forge/ucantone v0.0.0-20260827134420-25cf8340b9a1/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941 h1:MwZ/YKFKP1lXo+smPaFqQDqB0Tat+wa4BeI6o92FR8A= +github.com/fil-forge/ucantone v0.0.0-20260904190501-7fca40e13941/go.mod h1:aX35jlUs3hnWAhigjO8z9kMPiYnkW3XOW8iVoJyz7cg= github.com/filecoin-project/go-data-segment v0.0.1 h1:1wmDxOG4ubWQm3ZC1XI5nCon5qgSq7Ra3Rb6Dbu10Gs= github.com/filecoin-project/go-data-segment v0.0.1/go.mod h1:H0/NKbsRxmRFBcLibmABv+yFNHdmtl5AyplYLnb0Zv4= github.com/filecoin-project/go-fil-commcid v0.3.1 h1:4EfxpHSlvtkOqa9weG2Yt5kxFmPib2xU7Uc9Lbqk7fs= @@ -128,8 +126,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -148,8 +146,8 @@ github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ= -github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= @@ -318,10 +316,10 @@ go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= -golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk= +golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -334,8 +332,8 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= diff --git a/sprue/internal/fx/clients.go b/sprue/internal/fx/clients.go index 6fd91be3..3ac012f9 100644 --- a/sprue/internal/fx/clients.go +++ b/sprue/internal/fx/clients.go @@ -3,7 +3,7 @@ package fx import ( "net/url" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/internal/config" "github.com/fil-forge/forge/sprue/pkg/indexerclient" diff --git a/sprue/internal/fx/identity.go b/sprue/internal/fx/identity.go index 0b6e68b6..96ae2966 100644 --- a/sprue/internal/fx/identity.go +++ b/sprue/internal/fx/identity.go @@ -6,7 +6,7 @@ import ( "go.uber.org/fx" "go.uber.org/zap" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/internal/config" ) diff --git a/sprue/internal/fx/server.go b/sprue/internal/fx/server.go index a9dd86a8..df44fb29 100644 --- a/sprue/internal/fx/server.go +++ b/sprue/internal/fx/server.go @@ -12,7 +12,7 @@ import ( "go.uber.org/fx" "go.uber.org/zap" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/internal/config" "github.com/fil-forge/forge/sprue/pkg/build" "github.com/fil-forge/forge/sprue/pkg/service" diff --git a/sprue/internal/fx/server_test.go b/sprue/internal/fx/server_test.go index d90ae798..f83f57a2 100644 --- a/sprue/internal/fx/server_test.go +++ b/sprue/internal/fx/server_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/pkg/build" "github.com/labstack/echo/v4" "github.com/stretchr/testify/require" diff --git a/sprue/internal/fx/service/provider.go b/sprue/internal/fx/service/provider.go index 329cb28d..1e04790b 100644 --- a/sprue/internal/fx/service/provider.go +++ b/sprue/internal/fx/service/provider.go @@ -1,7 +1,7 @@ package service import ( - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/internal/config" "github.com/fil-forge/forge/sprue/pkg/service" diff --git a/sprue/internal/fx/services.go b/sprue/internal/fx/services.go index 83b0ad6d..3a1641cc 100644 --- a/sprue/internal/fx/services.go +++ b/sprue/internal/fx/services.go @@ -7,7 +7,7 @@ import ( "go.uber.org/fx" "go.uber.org/zap" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/internal/config" "github.com/fil-forge/forge/sprue/pkg/billing" "github.com/fil-forge/forge/sprue/pkg/mailer" diff --git a/sprue/internal/testutil/alias.go b/sprue/internal/testutil/alias.go index 17f71b91..0d1f1a8c 100644 --- a/sprue/internal/testutil/alias.go +++ b/sprue/internal/testutil/alias.go @@ -3,8 +3,8 @@ package testutil import ( "testing" - "github.com/fil-forge/libforge/identity" - "github.com/fil-forge/libforge/testutil" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/ucantone/testutil" "github.com/ipfs/go-cid" ) @@ -18,13 +18,13 @@ var ( WebServiceSigner = testutil.WebServiceSigner RandomBytes = testutil.RandomBytes RandomDID = testutil.RandomDID - RandomMultihash = testutil.RandomMultihash + RandomDigest = testutil.RandomDigest RandomIssuer = testutil.RandomIssuer RandomMultikeyIssuer = testutil.RandomMultikeyIssuer ) func RandomCID(t *testing.T) cid.Cid { - return cid.NewCidV1(cid.Raw, RandomMultihash(t)) + return cid.NewCidV1(cid.Raw, RandomDigest(t)) } func Must[T any](val T, err error) func(*testing.T) T { diff --git a/sprue/pkg/billing/service_test.go b/sprue/pkg/billing/service_test.go index 00a3acde..a3582fef 100644 --- a/sprue/pkg/billing/service_test.go +++ b/sprue/pkg/billing/service_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" + "github.com/fil-forge/forge/attestation/didmailto" customermemory "github.com/fil-forge/forge/sprue/pkg/store/customer/memory" "github.com/fil-forge/ucantone/did" "github.com/stretchr/testify/require" diff --git a/sprue/pkg/commands/admin/provider/deregister.go b/sprue/pkg/commands/admin/provider/deregister.go index 4843aa0c..3be8435d 100644 --- a/sprue/pkg/commands/admin/provider/deregister.go +++ b/sprue/pkg/commands/admin/provider/deregister.go @@ -3,7 +3,7 @@ package provider import ( - "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/forge/protocol/commands" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/ucan/command" ) diff --git a/sprue/pkg/commands/admin/provider/list.go b/sprue/pkg/commands/admin/provider/list.go index db17d22c..872f4aa4 100644 --- a/sprue/pkg/commands/admin/provider/list.go +++ b/sprue/pkg/commands/admin/provider/list.go @@ -3,7 +3,7 @@ package provider import ( - "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/forge/protocol/commands" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/ucan/command" ) diff --git a/sprue/pkg/commands/admin/provider/register.go b/sprue/pkg/commands/admin/provider/register.go index 4c7bf61d..abf0c516 100644 --- a/sprue/pkg/commands/admin/provider/register.go +++ b/sprue/pkg/commands/admin/provider/register.go @@ -3,7 +3,7 @@ package provider import ( - "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/forge/protocol/commands" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/ucan/command" ) diff --git a/sprue/pkg/commands/admin/provider/weight/set.go b/sprue/pkg/commands/admin/provider/weight/set.go index 0892f6c5..cde434b3 100644 --- a/sprue/pkg/commands/admin/provider/weight/set.go +++ b/sprue/pkg/commands/admin/provider/weight/set.go @@ -3,7 +3,7 @@ package weight import ( - "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/forge/protocol/commands" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/ucan/command" ) diff --git a/sprue/pkg/indexerclient/client.go b/sprue/pkg/indexerclient/client.go index d3e1e5dd..5e91f712 100644 --- a/sprue/pkg/indexerclient/client.go +++ b/sprue/pkg/indexerclient/client.go @@ -5,9 +5,8 @@ import ( "fmt" "net/url" - assertcmds "github.com/fil-forge/libforge/commands/assert" - contentcmds "github.com/fil-forge/libforge/commands/content" - ucanlib "github.com/fil-forge/libforge/ucan" + assertcmds "github.com/fil-forge/forge/protocol/commands/assert" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" "github.com/fil-forge/forge/sprue/pkg/lib/ucan_client" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" @@ -15,6 +14,7 @@ import ( "github.com/fil-forge/ucantone/ipld/datamodel" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/invocation" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" "go.uber.org/zap" ) diff --git a/sprue/pkg/lib/ucan_server/email_auth.go b/sprue/pkg/lib/ucan_server/email_auth.go index 8d924c86..2a0d4481 100644 --- a/sprue/pkg/lib/ucan_server/email_auth.go +++ b/sprue/pkg/lib/ucan_server/email_auth.go @@ -5,8 +5,8 @@ import ( "context" "fmt" - "github.com/fil-forge/libforge/attestation/didmailto" - "github.com/fil-forge/libforge/commands/access" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/ucantone/execution" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" diff --git a/sprue/pkg/lib/zapipld/raw_map_test.go b/sprue/pkg/lib/zapipld/raw_map_test.go index 70e3dc65..ceb3d0a1 100644 --- a/sprue/pkg/lib/zapipld/raw_map_test.go +++ b/sprue/pkg/lib/zapipld/raw_map_test.go @@ -4,8 +4,8 @@ import ( "bytes" "testing" - "github.com/fil-forge/libforge/testutil" "github.com/fil-forge/ucantone/ipld/datamodel" + "github.com/fil-forge/ucantone/testutil" "go.uber.org/zap" "go.uber.org/zap/zaptest" ) diff --git a/sprue/pkg/piriclient/client.go b/sprue/pkg/piriclient/client.go index 11c6605e..3f59f3d4 100644 --- a/sprue/pkg/piriclient/client.go +++ b/sprue/pkg/piriclient/client.go @@ -7,9 +7,8 @@ import ( "slices" "time" - blobcmds "github.com/fil-forge/libforge/commands/blob" - blobreplicacmds "github.com/fil-forge/libforge/commands/blob/replica" - ucanlib "github.com/fil-forge/libforge/ucan" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + blobreplicacmds "github.com/fil-forge/forge/protocol/commands/blob/replica" "github.com/fil-forge/forge/sprue/pkg/lib/ucan_client" "github.com/fil-forge/ucantone/client" "github.com/fil-forge/ucantone/did" @@ -17,6 +16,7 @@ import ( "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/invocation" "github.com/fil-forge/ucantone/ucan/promise" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" "github.com/multiformats/go-multihash" "go.uber.org/zap" diff --git a/sprue/pkg/provisioning/service_test.go b/sprue/pkg/provisioning/service_test.go index 9dc2634b..e1d7ab50 100644 --- a/sprue/pkg/provisioning/service_test.go +++ b/sprue/pkg/provisioning/service_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" + "github.com/fil-forge/forge/attestation/didmailto" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/provisioning" "github.com/fil-forge/forge/sprue/pkg/store/consumer" diff --git a/sprue/pkg/routing/service.go b/sprue/pkg/routing/service.go index 6c9dcefe..528be2ab 100644 --- a/sprue/pkg/routing/service.go +++ b/sprue/pkg/routing/service.go @@ -6,8 +6,8 @@ import ( "net/url" "slices" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/digestutil" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/forge/sprue/pkg/store" storageprovider "github.com/fil-forge/forge/sprue/pkg/store/storage_provider" "github.com/fil-forge/ucantone/did" diff --git a/sprue/pkg/routing/service_test.go b/sprue/pkg/routing/service_test.go index a95191c9..1aeb3e19 100644 --- a/sprue/pkg/routing/service_test.go +++ b/sprue/pkg/routing/service_test.go @@ -4,7 +4,7 @@ import ( "net/url" "testing" - "github.com/fil-forge/libforge/commands/blob" + "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/routing" storageprovider "github.com/fil-forge/forge/sprue/pkg/store/storage_provider" diff --git a/sprue/pkg/service/handlers/access_claim.go b/sprue/pkg/service/handlers/access_claim.go index 25458a5e..4c8b7ff4 100644 --- a/sprue/pkg/service/handlers/access_claim.go +++ b/sprue/pkg/service/handlers/access_claim.go @@ -3,8 +3,8 @@ package handlers import ( "fmt" - "github.com/fil-forge/libforge/commands/access" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/access" delegation_store "github.com/fil-forge/forge/sprue/pkg/store/delegation" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" diff --git a/sprue/pkg/service/handlers/access_claim_test.go b/sprue/pkg/service/handlers/access_claim_test.go index b8022d77..fb2c31ac 100644 --- a/sprue/pkg/service/handlers/access_claim_test.go +++ b/sprue/pkg/service/handlers/access_claim_test.go @@ -3,7 +3,7 @@ package handlers import ( "testing" - "github.com/fil-forge/libforge/commands/access" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/forge/sprue/internal/testutil" dlgmemory "github.com/fil-forge/forge/sprue/pkg/store/delegation/memory" "github.com/fil-forge/ucantone/execution" diff --git a/sprue/pkg/service/handlers/access_confirm.go b/sprue/pkg/service/handlers/access_confirm.go index df68b1e0..5487c1b9 100644 --- a/sprue/pkg/service/handlers/access_confirm.go +++ b/sprue/pkg/service/handlers/access_confirm.go @@ -6,10 +6,10 @@ import ( "github.com/ipfs/go-cid" "go.uber.org/zap" - "github.com/fil-forge/libforge/attestation" - "github.com/fil-forge/libforge/attestation/didmailto" - "github.com/fil-forge/libforge/commands/access" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/attestation" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/ipld/datamodel" diff --git a/sprue/pkg/service/handlers/access_confirm_test.go b/sprue/pkg/service/handlers/access_confirm_test.go index eb6815cb..33c562b8 100644 --- a/sprue/pkg/service/handlers/access_confirm_test.go +++ b/sprue/pkg/service/handlers/access_confirm_test.go @@ -3,9 +3,9 @@ package handlers import ( "testing" - "github.com/fil-forge/libforge/attestation" - "github.com/fil-forge/libforge/attestation/didmailto" - "github.com/fil-forge/libforge/commands/access" + "github.com/fil-forge/forge/attestation" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/forge/sprue/internal/testutil" dlgmemory "github.com/fil-forge/forge/sprue/pkg/store/delegation/memory" "github.com/fil-forge/ucantone/did" diff --git a/sprue/pkg/service/handlers/access_delegate.go b/sprue/pkg/service/handlers/access_delegate.go index c6af5e0a..f512dd95 100644 --- a/sprue/pkg/service/handlers/access_delegate.go +++ b/sprue/pkg/service/handlers/access_delegate.go @@ -3,7 +3,7 @@ package handlers import ( "fmt" - "github.com/fil-forge/libforge/commands/access" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/forge/sprue/pkg/provisioning" delegation_store "github.com/fil-forge/forge/sprue/pkg/store/delegation" "github.com/fil-forge/ucantone/binding" diff --git a/sprue/pkg/service/handlers/access_delegate_test.go b/sprue/pkg/service/handlers/access_delegate_test.go index 12af82da..47d70002 100644 --- a/sprue/pkg/service/handlers/access_delegate_test.go +++ b/sprue/pkg/service/handlers/access_delegate_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" - accesscmds "github.com/fil-forge/libforge/commands/access" - blobcmds "github.com/fil-forge/libforge/commands/blob" + "github.com/fil-forge/forge/attestation/didmailto" + accesscmds "github.com/fil-forge/forge/protocol/commands/access" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/provisioning" consumermemory "github.com/fil-forge/forge/sprue/pkg/store/consumer/memory" diff --git a/sprue/pkg/service/handlers/access_request.go b/sprue/pkg/service/handlers/access_request.go index 4e0692a3..3f14d08e 100644 --- a/sprue/pkg/service/handlers/access_request.go +++ b/sprue/pkg/service/handlers/access_request.go @@ -8,9 +8,9 @@ import ( "go.uber.org/zap" - "github.com/fil-forge/libforge/attestation/didmailto" - "github.com/fil-forge/libforge/commands/access" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/forge/sprue/internal/config" "github.com/fil-forge/forge/sprue/pkg/mailer" diff --git a/sprue/pkg/service/handlers/access_request_test.go b/sprue/pkg/service/handlers/access_request_test.go index c80d407d..c6c5ae67 100644 --- a/sprue/pkg/service/handlers/access_request_test.go +++ b/sprue/pkg/service/handlers/access_request_test.go @@ -6,9 +6,9 @@ import ( "net/url" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" - "github.com/fil-forge/libforge/commands/access" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands/access" "github.com/fil-forge/forge/sprue/internal/config" "github.com/fil-forge/forge/sprue/internal/testutil" diff --git a/sprue/pkg/service/handlers/admin_provider_deregister.go b/sprue/pkg/service/handlers/admin_provider_deregister.go index 40020132..0f172ce1 100644 --- a/sprue/pkg/service/handlers/admin_provider_deregister.go +++ b/sprue/pkg/service/handlers/admin_provider_deregister.go @@ -1,7 +1,7 @@ package handlers import ( - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/pkg/commands/admin/provider" storageprovider "github.com/fil-forge/forge/sprue/pkg/store/storage_provider" "github.com/fil-forge/ucantone/binding" diff --git a/sprue/pkg/service/handlers/admin_provider_deregister_test.go b/sprue/pkg/service/handlers/admin_provider_deregister_test.go index 56a7f0f4..4310e807 100644 --- a/sprue/pkg/service/handlers/admin_provider_deregister_test.go +++ b/sprue/pkg/service/handlers/admin_provider_deregister_test.go @@ -4,8 +4,8 @@ import ( "net/url" "testing" - blobcmds "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/commands/admin/provider" "github.com/fil-forge/forge/sprue/pkg/service/handlers" diff --git a/sprue/pkg/service/handlers/admin_provider_list.go b/sprue/pkg/service/handlers/admin_provider_list.go index 352f6e1e..9aa626e1 100644 --- a/sprue/pkg/service/handlers/admin_provider_list.go +++ b/sprue/pkg/service/handlers/admin_provider_list.go @@ -5,7 +5,7 @@ import ( "go.uber.org/zap" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/pkg/commands/admin/provider" "github.com/fil-forge/forge/sprue/pkg/store" storageprovider "github.com/fil-forge/forge/sprue/pkg/store/storage_provider" diff --git a/sprue/pkg/service/handlers/admin_provider_list_test.go b/sprue/pkg/service/handlers/admin_provider_list_test.go index e5d872bc..a611390b 100644 --- a/sprue/pkg/service/handlers/admin_provider_list_test.go +++ b/sprue/pkg/service/handlers/admin_provider_list_test.go @@ -4,7 +4,7 @@ import ( "net/url" "testing" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/commands/admin/provider" "github.com/fil-forge/forge/sprue/pkg/service/handlers" diff --git a/sprue/pkg/service/handlers/admin_provider_register.go b/sprue/pkg/service/handlers/admin_provider_register.go index 222acfd5..e8cdc124 100644 --- a/sprue/pkg/service/handlers/admin_provider_register.go +++ b/sprue/pkg/service/handlers/admin_provider_register.go @@ -3,11 +3,10 @@ package handlers import ( "net/url" - blobcmds "github.com/fil-forge/libforge/commands/blob" - replicacmds "github.com/fil-forge/libforge/commands/blob/replica" - pdpcmds "github.com/fil-forge/libforge/commands/pdp" - "github.com/fil-forge/libforge/identity" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + replicacmds "github.com/fil-forge/forge/protocol/commands/blob/replica" + pdpcmds "github.com/fil-forge/forge/protocol/commands/pdp" "github.com/fil-forge/forge/sprue/pkg/commands/admin/provider" storageprovider "github.com/fil-forge/forge/sprue/pkg/store/storage_provider" "github.com/fil-forge/ucantone/binding" @@ -15,6 +14,7 @@ import ( "github.com/fil-forge/ucantone/server" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/container" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "go.uber.org/zap" ) diff --git a/sprue/pkg/service/handlers/admin_provider_register_test.go b/sprue/pkg/service/handlers/admin_provider_register_test.go index ee41f10f..f1845e04 100644 --- a/sprue/pkg/service/handlers/admin_provider_register_test.go +++ b/sprue/pkg/service/handlers/admin_provider_register_test.go @@ -3,10 +3,10 @@ package handlers_test import ( "testing" - blobcmds "github.com/fil-forge/libforge/commands/blob" - replicacmds "github.com/fil-forge/libforge/commands/blob/replica" - pdpcmds "github.com/fil-forge/libforge/commands/pdp" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + replicacmds "github.com/fil-forge/forge/protocol/commands/blob/replica" + pdpcmds "github.com/fil-forge/forge/protocol/commands/pdp" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/commands/admin/provider" "github.com/fil-forge/forge/sprue/pkg/service/handlers" diff --git a/sprue/pkg/service/handlers/admin_provider_weight_set.go b/sprue/pkg/service/handlers/admin_provider_weight_set.go index 68e82695..0dd0b93b 100644 --- a/sprue/pkg/service/handlers/admin_provider_weight_set.go +++ b/sprue/pkg/service/handlers/admin_provider_weight_set.go @@ -3,7 +3,7 @@ package handlers import ( "go.uber.org/zap" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/pkg/commands/admin/provider/weight" storageprovider "github.com/fil-forge/forge/sprue/pkg/store/storage_provider" "github.com/fil-forge/ucantone/binding" diff --git a/sprue/pkg/service/handlers/admin_provider_weight_set_test.go b/sprue/pkg/service/handlers/admin_provider_weight_set_test.go index 78d5d897..a03b8799 100644 --- a/sprue/pkg/service/handlers/admin_provider_weight_set_test.go +++ b/sprue/pkg/service/handlers/admin_provider_weight_set_test.go @@ -4,8 +4,8 @@ import ( "net/url" "testing" - blobcmds "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/commands/admin/provider/weight" "github.com/fil-forge/forge/sprue/pkg/service/handlers" diff --git a/sprue/pkg/service/handlers/blob_abort.go b/sprue/pkg/service/handlers/blob_abort.go index eb20fd9a..67bf377c 100644 --- a/sprue/pkg/service/handlers/blob_abort.go +++ b/sprue/pkg/service/handlers/blob_abort.go @@ -3,9 +3,8 @@ package handlers import ( "fmt" - blobcmds "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/digestutil" - ucanlib "github.com/fil-forge/libforge/ucan" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/forge/sprue/pkg/piriclient" "github.com/fil-forge/forge/sprue/pkg/routing" "github.com/fil-forge/forge/sprue/pkg/store/agent" @@ -13,6 +12,7 @@ import ( "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/server" "github.com/fil-forge/ucantone/ucan" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "go.uber.org/zap" ) diff --git a/sprue/pkg/service/handlers/blob_abort_test.go b/sprue/pkg/service/handlers/blob_abort_test.go index c5bf39d9..03a1d112 100644 --- a/sprue/pkg/service/handlers/blob_abort_test.go +++ b/sprue/pkg/service/handlers/blob_abort_test.go @@ -6,9 +6,9 @@ import ( "sync" "testing" - blobcmds "github.com/fil-forge/libforge/commands/blob" - httpcmds "github.com/fil-forge/libforge/commands/http" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + httpcmds "github.com/fil-forge/forge/protocol/commands/http" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/piriclient" "github.com/fil-forge/forge/sprue/pkg/routing" @@ -194,7 +194,7 @@ func TestBlobAbortHandler(t *testing.T) { t.Run("forwards to the parked blob's provider", func(t *testing.T) { deps := newBlobAbortTestDeps(t, uploadService, logger) space := testutil.RandomIssuer(t) - blob := blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024} + blob := blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024} storageProvider := testutil.RandomIssuer(t) piriSrv := newMockPiriRejectServer(t, storageProvider, identity.Identity{Issuer: uploadService}) @@ -225,7 +225,7 @@ func TestBlobAbortHandler(t *testing.T) { _, err := blobcmds.Abort.Invoke( testutil.Alice, space.DID(), - &blobcmds.AbortArguments{Digest: testutil.RandomMultihash(t), Cause: cid.Undef}, + &blobcmds.AbortArguments{Digest: testutil.RandomDigest(t), Cause: cid.Undef}, invocation.WithAudience(uploadService.DID()), ) require.ErrorContains(t, err, "undefined cid") @@ -236,7 +236,7 @@ func TestBlobAbortHandler(t *testing.T) { space := testutil.RandomIssuer(t) cause := testutil.RandomCID(t) - rcpt, herr := invokeBlobAbort(t, deps, uploadService, space, testutil.RandomMultihash(t), cause) + rcpt, herr := invokeBlobAbort(t, deps, uploadService, space, testutil.RandomDigest(t), cause) require.NoError(t, herr) _, err := blobcmds.Abort.Unpack(rcpt) var named errors.Named @@ -248,7 +248,7 @@ func TestBlobAbortHandler(t *testing.T) { t.Run("surfaces the node's BlobAccepted refusal", func(t *testing.T) { deps := newBlobAbortTestDeps(t, uploadService, logger) space := testutil.RandomIssuer(t) - blob := blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024} + blob := blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024} storageProvider := testutil.RandomIssuer(t) piriSrv := newMockPiriRejectServer(t, storageProvider, identity.Identity{Issuer: uploadService}) diff --git a/sprue/pkg/service/handlers/blob_add.go b/sprue/pkg/service/handlers/blob_add.go index 4f16ba1f..963f69b0 100644 --- a/sprue/pkg/service/handlers/blob_add.go +++ b/sprue/pkg/service/handlers/blob_add.go @@ -6,12 +6,11 @@ import ( "crypto/ed25519" "fmt" - accesscmds "github.com/fil-forge/libforge/commands/access" - blobcmds "github.com/fil-forge/libforge/commands/blob" - httpcmds "github.com/fil-forge/libforge/commands/http" - "github.com/fil-forge/libforge/digestutil" - "github.com/fil-forge/libforge/identity" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/internal/identity" + accesscmds "github.com/fil-forge/forge/protocol/commands/access" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + httpcmds "github.com/fil-forge/forge/protocol/commands/http" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/forge/sprue/pkg/piriclient" "github.com/fil-forge/forge/sprue/pkg/provisioning" "github.com/fil-forge/forge/sprue/pkg/routing" @@ -29,6 +28,7 @@ import ( "github.com/fil-forge/ucantone/ucan/invocation" "github.com/fil-forge/ucantone/ucan/promise" "github.com/fil-forge/ucantone/ucan/receipt" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" "github.com/multiformats/go-multihash" "go.uber.org/zap" diff --git a/sprue/pkg/service/handlers/blob_add_test.go b/sprue/pkg/service/handlers/blob_add_test.go index 23b1d60c..e1bc8d1e 100644 --- a/sprue/pkg/service/handlers/blob_add_test.go +++ b/sprue/pkg/service/handlers/blob_add_test.go @@ -7,12 +7,12 @@ import ( "testing" "time" - "github.com/fil-forge/libforge/attestation/didmailto" - "github.com/fil-forge/libforge/commands" - accesscmds "github.com/fil-forge/libforge/commands/access" - blobcmds "github.com/fil-forge/libforge/commands/blob" - httpcmds "github.com/fil-forge/libforge/commands/http" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/forge/internal/identity" + "github.com/fil-forge/forge/protocol/commands" + accesscmds "github.com/fil-forge/forge/protocol/commands/access" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + httpcmds "github.com/fil-forge/forge/protocol/commands/http" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/piriclient" "github.com/fil-forge/forge/sprue/pkg/provisioning" @@ -166,7 +166,7 @@ func TestBlobAddHandler(t *testing.T) { space := testutil.RandomIssuer(t) args := blobcmds.AddArguments{ - Blob: blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024}, + Blob: blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024}, } inv, err := blobcmds.Add.Invoke( @@ -198,7 +198,7 @@ func TestBlobAddHandler(t *testing.T) { // No storage providers in spStore — the router will return ErrCandidateUnavailable. args := blobcmds.AddArguments{ - Blob: blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024}, + Blob: blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024}, } inv, err := blobcmds.Add.Invoke( @@ -233,7 +233,7 @@ func TestBlobAddHandler(t *testing.T) { require.NoError(t, err) args := blobcmds.AddArguments{ - Blob: blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024}, + Blob: blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024}, } inv, err := blobcmds.Add.Invoke( @@ -288,7 +288,7 @@ func TestBlobAddHandler(t *testing.T) { require.NoError(t, err) args := blobcmds.AddArguments{ - Blob: blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024}, + Blob: blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024}, } inv, err := blobcmds.Add.Invoke( @@ -336,7 +336,7 @@ func TestBlobAddHandler(t *testing.T) { require.NoError(t, err) args := blobcmds.AddArguments{ - Blob: blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024}, + Blob: blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024}, } inv, err := blobcmds.Add.Invoke( @@ -370,7 +370,7 @@ func TestBlobAddHandler(t *testing.T) { provisionSpace(t, deps, uploadService, space.DID()) storageProvider := testutil.RandomIssuer(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) blob := blobcmds.Blob{Digest: digest, Size: 1024} // Build the chain that the handler will walk back through: diff --git a/sprue/pkg/service/handlers/blob_list.go b/sprue/pkg/service/handlers/blob_list.go index 621c3cf9..aff85a5c 100644 --- a/sprue/pkg/service/handlers/blob_list.go +++ b/sprue/pkg/service/handlers/blob_list.go @@ -3,7 +3,7 @@ package handlers import ( "fmt" - blobcmds "github.com/fil-forge/libforge/commands/blob" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" blobregistry "github.com/fil-forge/forge/sprue/pkg/store/blob_registry" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" diff --git a/sprue/pkg/service/handlers/blob_list_test.go b/sprue/pkg/service/handlers/blob_list_test.go index 32fadd4a..276b4f28 100644 --- a/sprue/pkg/service/handlers/blob_list_test.go +++ b/sprue/pkg/service/handlers/blob_list_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" - blobcmds "github.com/fil-forge/libforge/commands/blob" + "github.com/fil-forge/forge/attestation/didmailto" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/service/handlers" blob_registry "github.com/fil-forge/forge/sprue/pkg/store/blob_registry/memory" @@ -85,8 +85,8 @@ func TestBlobListHandler(t *testing.T) { space := testutil.RandomIssuer(t) require.NoError(t, consumerStore.Add(ctx, uploadService.DID(), space.DID(), aliceAccount, "sub-1", testutil.RandomCID(t))) - blob1 := blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 100} - blob2 := blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 200} + blob1 := blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 100} + blob2 := blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 200} require.NoError(t, blobReg.Register(ctx, space.DID(), blob1, testutil.RandomCID(t))) require.NoError(t, blobReg.Register(ctx, space.DID(), blob2, testutil.RandomCID(t))) @@ -110,7 +110,7 @@ func TestBlobListHandler(t *testing.T) { for i := range 3 { require.NoError(t, blobReg.Register( ctx, space.DID(), - blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: uint64(i + 1)}, + blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: uint64(i + 1)}, testutil.RandomCID(t), )) } @@ -137,7 +137,7 @@ func TestBlobListHandler(t *testing.T) { for i := range 3 { require.NoError(t, blobReg.Register( ctx, space.DID(), - blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: uint64(i + 1)}, + blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: uint64(i + 1)}, testutil.RandomCID(t), )) } @@ -172,7 +172,7 @@ func TestBlobListHandler(t *testing.T) { require.NoError(t, blobReg.Register( ctx, space1.DID(), - blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 100}, + blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 100}, testutil.RandomCID(t), )) diff --git a/sprue/pkg/service/handlers/blob_remove.go b/sprue/pkg/service/handlers/blob_remove.go index f3ced06d..2fc2acbc 100644 --- a/sprue/pkg/service/handlers/blob_remove.go +++ b/sprue/pkg/service/handlers/blob_remove.go @@ -5,9 +5,8 @@ import ( "context" "fmt" - blobcmds "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/digestutil" - ucanlib "github.com/fil-forge/libforge/ucan" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/forge/sprue/pkg/piriclient" "github.com/fil-forge/forge/sprue/pkg/routing" "github.com/fil-forge/forge/sprue/pkg/store/agent" @@ -18,6 +17,7 @@ import ( "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/server" "github.com/fil-forge/ucantone/ucan" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "github.com/ipfs/go-cid" "github.com/multiformats/go-multihash" "go.uber.org/zap" diff --git a/sprue/pkg/service/handlers/blob_remove_test.go b/sprue/pkg/service/handlers/blob_remove_test.go index 69a73ad3..d2e15c82 100644 --- a/sprue/pkg/service/handlers/blob_remove_test.go +++ b/sprue/pkg/service/handlers/blob_remove_test.go @@ -6,10 +6,10 @@ import ( "sync" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" - blobcmds "github.com/fil-forge/libforge/commands/blob" - httpcmds "github.com/fil-forge/libforge/commands/http" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + httpcmds "github.com/fil-forge/forge/protocol/commands/http" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/piriclient" "github.com/fil-forge/forge/sprue/pkg/routing" @@ -237,7 +237,7 @@ func TestBlobRemoveHandler(t *testing.T) { deps := newBlobRemoveTestDeps(t, uploadService, logger) space := testutil.RandomIssuer(t) - rcpt := invokeBlobRemove(t, deps, uploadService, space, testutil.RandomMultihash(t)) + rcpt := invokeBlobRemove(t, deps, uploadService, space, testutil.RandomDigest(t)) _, err := blobcmds.Remove.Unpack(rcpt) require.NoError(t, err) }) @@ -245,7 +245,7 @@ func TestBlobRemoveHandler(t *testing.T) { t.Run("forwards to primary provider and deregisters", func(t *testing.T) { deps := newBlobRemoveTestDeps(t, uploadService, logger) space := testutil.RandomIssuer(t) - blob := blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024} + blob := blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024} storageProvider := testutil.RandomIssuer(t) piriSrv := newMockPiriReleaseServer(t, storageProvider, identity.Identity{Issuer: uploadService}) @@ -291,7 +291,7 @@ func TestBlobRemoveHandler(t *testing.T) { t.Run("forwards to replicas too", func(t *testing.T) { deps := newBlobRemoveTestDeps(t, uploadService, logger) space := testutil.RandomIssuer(t) - blob := blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024} + blob := blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024} primary := testutil.RandomIssuer(t) primarySrv := newMockPiriReleaseServer(t, primary, identity.Identity{Issuer: uploadService}) @@ -320,7 +320,7 @@ func TestBlobRemoveHandler(t *testing.T) { t.Run("unreachable provider does not fail the removal", func(t *testing.T) { deps := newBlobRemoveTestDeps(t, uploadService, logger) space := testutil.RandomIssuer(t) - blob := blobcmds.Blob{Digest: testutil.RandomMultihash(t), Size: 1024} + blob := blobcmds.Blob{Digest: testutil.RandomDigest(t), Size: 1024} // The primary provider is registered with an endpoint nothing // listens on. diff --git a/sprue/pkg/service/handlers/blob_replicate_test.go b/sprue/pkg/service/handlers/blob_replicate_test.go index 70a7ad32..97273962 100644 --- a/sprue/pkg/service/handlers/blob_replicate_test.go +++ b/sprue/pkg/service/handlers/blob_replicate_test.go @@ -7,7 +7,7 @@ package handlers_test // "testing" // "time" -// "github.com/fil-forge/libforge/attestation/didmailto" +// "github.com/fil-forge/forge/attestation/didmailto" // "github.com/fil-forge/ucantone/did" // cidlink "github.com/ipld/go-ipld-prime/linking/cid" // "github.com/storacha/go-libstoracha/capabilities/assert" @@ -64,7 +64,7 @@ package handlers_test // router, blobReg, replicaStore, agentStore, nodeProvider, logger, // ) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // site := cidlink.Link{Cid: testutil.RandomCID(t)} @@ -103,7 +103,7 @@ package handlers_test // ) // space := testutil.RandomSigner(t) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // site := cidlink.Link{Cid: testutil.RandomCID(t)} @@ -141,7 +141,7 @@ package handlers_test // ) // space := testutil.RandomSigner(t) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // site := cidlink.Link{Cid: testutil.RandomCID(t)} @@ -184,7 +184,7 @@ package handlers_test // err := consumerStore.Add(ctx, uploadService.DID(), space.DID(), aliceAccount, "", testutil.RandomCID(t)) // require.NoError(t, err) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // // random CID is not a valid location commitment delegation // site := cidlink.Link{Cid: testutil.RandomCID(t)} @@ -294,7 +294,7 @@ package handlers_test // err = consumerStore.Add(ctx, uploadService.DID(), space.DID(), aliceAccount, "", testutil.RandomCID(t)) // require.NoError(t, err) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // // register the blob in the space @@ -396,7 +396,7 @@ package handlers_test // err = consumerStore.Add(ctx, uploadService.DID(), space.DID(), aliceAccount, "", testutil.RandomCID(t)) // require.NoError(t, err) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // site := cidlink.Link{Cid: testutil.RandomCID(t)} diff --git a/sprue/pkg/service/handlers/customer_add.go b/sprue/pkg/service/handlers/customer_add.go index 72768114..13da0444 100644 --- a/sprue/pkg/service/handlers/customer_add.go +++ b/sprue/pkg/service/handlers/customer_add.go @@ -3,8 +3,8 @@ package handlers import ( "fmt" - customercmds "github.com/fil-forge/libforge/commands/customer" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + customercmds "github.com/fil-forge/forge/protocol/commands/customer" customerstore "github.com/fil-forge/forge/sprue/pkg/store/customer" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/errors" diff --git a/sprue/pkg/service/handlers/customer_add_test.go b/sprue/pkg/service/handlers/customer_add_test.go index 6975e9c6..22bd9a3b 100644 --- a/sprue/pkg/service/handlers/customer_add_test.go +++ b/sprue/pkg/service/handlers/customer_add_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - customercmds "github.com/fil-forge/libforge/commands/customer" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + customercmds "github.com/fil-forge/forge/protocol/commands/customer" "github.com/fil-forge/forge/sprue/internal/testutil" customerstore "github.com/fil-forge/forge/sprue/pkg/store/customer" customer_store "github.com/fil-forge/forge/sprue/pkg/store/customer/memory" diff --git a/sprue/pkg/service/handlers/index_add.go b/sprue/pkg/service/handlers/index_add.go index 52b10be1..261922fd 100644 --- a/sprue/pkg/service/handlers/index_add.go +++ b/sprue/pkg/service/handlers/index_add.go @@ -5,16 +5,16 @@ import ( "go.uber.org/zap" - accesscmds "github.com/fil-forge/libforge/commands/access" - indexcmds "github.com/fil-forge/libforge/commands/index" - "github.com/fil-forge/libforge/identity" - ucanlib "github.com/fil-forge/libforge/ucan" + "github.com/fil-forge/forge/internal/identity" + accesscmds "github.com/fil-forge/forge/protocol/commands/access" + indexcmds "github.com/fil-forge/forge/protocol/commands/index" "github.com/fil-forge/forge/sprue/pkg/indexerclient" "github.com/fil-forge/forge/sprue/pkg/provisioning" blobregistry "github.com/fil-forge/forge/sprue/pkg/store/blob_registry" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/server" + ucanlib "github.com/fil-forge/ucantone/ucanlib" ) const IndexerNotConfiguredErrorName = "IndexerNotConfigured" diff --git a/sprue/pkg/service/handlers/index_add_test.go b/sprue/pkg/service/handlers/index_add_test.go index 4cf07a07..01c80818 100644 --- a/sprue/pkg/service/handlers/index_add_test.go +++ b/sprue/pkg/service/handlers/index_add_test.go @@ -6,13 +6,13 @@ import ( "net/url" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" - accesscmds "github.com/fil-forge/libforge/commands/access" - assertcmds "github.com/fil-forge/libforge/commands/assert" - blobcmds "github.com/fil-forge/libforge/commands/blob" - contentcmds "github.com/fil-forge/libforge/commands/content" - indexcmds "github.com/fil-forge/libforge/commands/index" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/forge/internal/identity" + accesscmds "github.com/fil-forge/forge/protocol/commands/access" + assertcmds "github.com/fil-forge/forge/protocol/commands/assert" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + contentcmds "github.com/fil-forge/forge/protocol/commands/content" + indexcmds "github.com/fil-forge/forge/protocol/commands/index" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/indexerclient" "github.com/fil-forge/forge/sprue/pkg/provisioning" diff --git a/sprue/pkg/service/handlers/provider_add.go b/sprue/pkg/service/handlers/provider_add.go index de522453..8ce97d8a 100644 --- a/sprue/pkg/service/handlers/provider_add.go +++ b/sprue/pkg/service/handlers/provider_add.go @@ -3,8 +3,8 @@ package handlers import ( "fmt" - "github.com/fil-forge/libforge/attestation/didmailto" - providercmds "github.com/fil-forge/libforge/commands/provider" + "github.com/fil-forge/forge/attestation/didmailto" + providercmds "github.com/fil-forge/forge/protocol/commands/provider" "github.com/fil-forge/forge/sprue/internal/config" "github.com/fil-forge/forge/sprue/pkg/billing" "github.com/fil-forge/forge/sprue/pkg/provisioning" diff --git a/sprue/pkg/service/handlers/provider_add_test.go b/sprue/pkg/service/handlers/provider_add_test.go index c1af80d1..c54e9547 100644 --- a/sprue/pkg/service/handlers/provider_add_test.go +++ b/sprue/pkg/service/handlers/provider_add_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" - providercmds "github.com/fil-forge/libforge/commands/provider" + "github.com/fil-forge/forge/attestation/didmailto" + providercmds "github.com/fil-forge/forge/protocol/commands/provider" "github.com/fil-forge/forge/sprue/internal/config" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/billing" diff --git a/sprue/pkg/service/handlers/space_info.go b/sprue/pkg/service/handlers/space_info.go index 85bb8379..2fbdf5a4 100644 --- a/sprue/pkg/service/handlers/space_info.go +++ b/sprue/pkg/service/handlers/space_info.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - spacecmds "github.com/fil-forge/libforge/commands/space" + spacecmds "github.com/fil-forge/forge/protocol/commands/space" "github.com/fil-forge/forge/sprue/pkg/provisioning" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/errors" diff --git a/sprue/pkg/service/handlers/space_info_test.go b/sprue/pkg/service/handlers/space_info_test.go index d6c7b0df..f2a72b43 100644 --- a/sprue/pkg/service/handlers/space_info_test.go +++ b/sprue/pkg/service/handlers/space_info_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" - spacecmds "github.com/fil-forge/libforge/commands/space" + "github.com/fil-forge/forge/attestation/didmailto" + spacecmds "github.com/fil-forge/forge/protocol/commands/space" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/provisioning" "github.com/fil-forge/forge/sprue/pkg/service/handlers" diff --git a/sprue/pkg/service/handlers/ucan_conclude.go b/sprue/pkg/service/handlers/ucan_conclude.go index 92689d16..57b02e8f 100644 --- a/sprue/pkg/service/handlers/ucan_conclude.go +++ b/sprue/pkg/service/handlers/ucan_conclude.go @@ -6,8 +6,8 @@ import ( "maps" "slices" - ucancmds "github.com/fil-forge/libforge/commands/ucan" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + ucancmds "github.com/fil-forge/forge/protocol/commands/ucan" "github.com/fil-forge/forge/sprue/pkg/store/agent" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/errors" diff --git a/sprue/pkg/service/handlers/ucan_conclude_blob_replica_transfer_test.go b/sprue/pkg/service/handlers/ucan_conclude_blob_replica_transfer_test.go index 7df91d09..e70fcb7d 100644 --- a/sprue/pkg/service/handlers/ucan_conclude_blob_replica_transfer_test.go +++ b/sprue/pkg/service/handlers/ucan_conclude_blob_replica_transfer_test.go @@ -101,7 +101,7 @@ package handlers_test // ) // space := testutil.RandomSigner(t) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // site := cidlink.Link{Cid: testutil.RandomCID(t)} // // cause points to a non-existent allocation invocation @@ -141,7 +141,7 @@ package handlers_test // ) // space := testutil.RandomSigner(t) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // site := cidlink.Link{Cid: testutil.RandomCID(t)} // replicateCause := cidlink.Link{Cid: testutil.RandomCID(t)} @@ -206,7 +206,7 @@ package handlers_test // ) // space := testutil.RandomSigner(t) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // site := cidlink.Link{Cid: testutil.RandomCID(t)} // replicateCause := cidlink.Link{Cid: testutil.RandomCID(t)} @@ -292,7 +292,7 @@ package handlers_test // ) // space := testutil.RandomSigner(t) -// digest := testutil.RandomMultihash(t) +// digest := testutil.RandomDigest(t) // blob := types.Blob{Digest: digest, Size: 1024} // site := cidlink.Link{Cid: testutil.RandomCID(t)} // replicateCause := cidlink.Link{Cid: testutil.RandomCID(t)} diff --git a/sprue/pkg/service/handlers/ucan_conclude_http_put.go b/sprue/pkg/service/handlers/ucan_conclude_http_put.go index f6de4903..497b66d1 100644 --- a/sprue/pkg/service/handlers/ucan_conclude_http_put.go +++ b/sprue/pkg/service/handlers/ucan_conclude_http_put.go @@ -5,11 +5,10 @@ import ( "context" "fmt" - blobcmds "github.com/fil-forge/libforge/commands/blob" - httpcmds "github.com/fil-forge/libforge/commands/http" - ucancmds "github.com/fil-forge/libforge/commands/ucan" - "github.com/fil-forge/libforge/digestutil" - ucanlib "github.com/fil-forge/libforge/ucan" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + httpcmds "github.com/fil-forge/forge/protocol/commands/http" + ucancmds "github.com/fil-forge/forge/protocol/commands/ucan" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/forge/sprue/pkg/piriclient" "github.com/fil-forge/forge/sprue/pkg/routing" "github.com/fil-forge/forge/sprue/pkg/store/agent" @@ -18,6 +17,7 @@ import ( edm "github.com/fil-forge/ucantone/errors/datamodel" "github.com/fil-forge/ucantone/ucan" "github.com/fil-forge/ucantone/ucan/invocation" + ucanlib "github.com/fil-forge/ucantone/ucanlib" "go.uber.org/zap" ) diff --git a/sprue/pkg/service/handlers/ucan_conclude_http_put_test.go b/sprue/pkg/service/handlers/ucan_conclude_http_put_test.go index 36394f3d..18994b0a 100644 --- a/sprue/pkg/service/handlers/ucan_conclude_http_put_test.go +++ b/sprue/pkg/service/handlers/ucan_conclude_http_put_test.go @@ -4,9 +4,9 @@ import ( "net/url" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" - blobcmds "github.com/fil-forge/libforge/commands/blob" - httpcmds "github.com/fil-forge/libforge/commands/http" + "github.com/fil-forge/forge/attestation/didmailto" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + httpcmds "github.com/fil-forge/forge/protocol/commands/http" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/piriclient" "github.com/fil-forge/forge/sprue/pkg/routing" @@ -66,7 +66,7 @@ func TestHTTPPutConcludeHandler(t *testing.T) { t.Run("allocation invocation not found", func(t *testing.T) { deps := newHTTPPutDeps(t, piriclient.NewProvider(uploadService, logger), logger) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) // Destination.Task points to an invocation that's not in the agent store. nonExistentAllocTask := testutil.RandomCID(t) @@ -99,7 +99,7 @@ func TestHTTPPutConcludeHandler(t *testing.T) { storageProvider := testutil.RandomIssuer(t) space := testutil.RandomIssuer(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) blob := blobcmds.Blob{Digest: digest, Size: 1024} // Persist a /blob/allocate invocation for the storage provider, but do @@ -149,7 +149,7 @@ func TestHTTPPutConcludeHandler(t *testing.T) { t.Run("success registers blob in space", func(t *testing.T) { storageProvider := testutil.RandomIssuer(t) space := testutil.RandomIssuer(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) blob := blobcmds.Blob{Digest: digest, Size: 1024} blobAddTaskLink := testutil.RandomCID(t) diff --git a/sprue/pkg/service/handlers/ucan_conclude_test.go b/sprue/pkg/service/handlers/ucan_conclude_test.go index 7f7472ff..0803c160 100644 --- a/sprue/pkg/service/handlers/ucan_conclude_test.go +++ b/sprue/pkg/service/handlers/ucan_conclude_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - blobcmds "github.com/fil-forge/libforge/commands/blob" - ucancmds "github.com/fil-forge/libforge/commands/ucan" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/internal/identity" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + ucancmds "github.com/fil-forge/forge/protocol/commands/ucan" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/service/handlers" "github.com/fil-forge/forge/sprue/pkg/store/agent" diff --git a/sprue/pkg/service/handlers/upload_add.go b/sprue/pkg/service/handlers/upload_add.go index 7bb83844..8bc243ee 100644 --- a/sprue/pkg/service/handlers/upload_add.go +++ b/sprue/pkg/service/handlers/upload_add.go @@ -3,8 +3,8 @@ package handlers import ( "fmt" - accesscmds "github.com/fil-forge/libforge/commands/access" - uploadcmds "github.com/fil-forge/libforge/commands/upload" + accesscmds "github.com/fil-forge/forge/protocol/commands/access" + uploadcmds "github.com/fil-forge/forge/protocol/commands/upload" "github.com/fil-forge/forge/sprue/pkg/provisioning" upload_store "github.com/fil-forge/forge/sprue/pkg/store/upload" "github.com/fil-forge/ucantone/binding" diff --git a/sprue/pkg/service/handlers/upload_add_test.go b/sprue/pkg/service/handlers/upload_add_test.go index acee1fe9..ffcd9650 100644 --- a/sprue/pkg/service/handlers/upload_add_test.go +++ b/sprue/pkg/service/handlers/upload_add_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - "github.com/fil-forge/libforge/attestation/didmailto" - accesscmds "github.com/fil-forge/libforge/commands/access" - blobcmds "github.com/fil-forge/libforge/commands/blob" - uploadcmds "github.com/fil-forge/libforge/commands/upload" + "github.com/fil-forge/forge/attestation/didmailto" + accesscmds "github.com/fil-forge/forge/protocol/commands/access" + blobcmds "github.com/fil-forge/forge/protocol/commands/blob" + uploadcmds "github.com/fil-forge/forge/protocol/commands/upload" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/provisioning" "github.com/fil-forge/forge/sprue/pkg/service/handlers" diff --git a/sprue/pkg/service/handlers/upload_list.go b/sprue/pkg/service/handlers/upload_list.go index 144f2be7..41fc6d30 100644 --- a/sprue/pkg/service/handlers/upload_list.go +++ b/sprue/pkg/service/handlers/upload_list.go @@ -3,7 +3,7 @@ package handlers import ( "fmt" - uploadcmds "github.com/fil-forge/libforge/commands/upload" + uploadcmds "github.com/fil-forge/forge/protocol/commands/upload" upload_store "github.com/fil-forge/forge/sprue/pkg/store/upload" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" diff --git a/sprue/pkg/service/handlers/upload_list_test.go b/sprue/pkg/service/handlers/upload_list_test.go index 4ae83189..b3ed0a41 100644 --- a/sprue/pkg/service/handlers/upload_list_test.go +++ b/sprue/pkg/service/handlers/upload_list_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - uploadcmds "github.com/fil-forge/libforge/commands/upload" + uploadcmds "github.com/fil-forge/forge/protocol/commands/upload" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/service/handlers" upload_store "github.com/fil-forge/forge/sprue/pkg/store/upload/memory" diff --git a/sprue/pkg/service/handlers/upload_remove.go b/sprue/pkg/service/handlers/upload_remove.go index 5c8cc8a0..82458bea 100644 --- a/sprue/pkg/service/handlers/upload_remove.go +++ b/sprue/pkg/service/handlers/upload_remove.go @@ -3,7 +3,7 @@ package handlers import ( "fmt" - uploadcmds "github.com/fil-forge/libforge/commands/upload" + uploadcmds "github.com/fil-forge/forge/protocol/commands/upload" upload_store "github.com/fil-forge/forge/sprue/pkg/store/upload" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/errors" diff --git a/sprue/pkg/service/handlers/upload_remove_test.go b/sprue/pkg/service/handlers/upload_remove_test.go index 0abc5776..1c6c76c2 100644 --- a/sprue/pkg/service/handlers/upload_remove_test.go +++ b/sprue/pkg/service/handlers/upload_remove_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - uploadcmds "github.com/fil-forge/libforge/commands/upload" + uploadcmds "github.com/fil-forge/forge/protocol/commands/upload" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/service/handlers" upload_store "github.com/fil-forge/forge/sprue/pkg/store/upload/memory" diff --git a/sprue/pkg/service/handlers/upload_shard_list.go b/sprue/pkg/service/handlers/upload_shard_list.go index fedecdab..afbbe63f 100644 --- a/sprue/pkg/service/handlers/upload_shard_list.go +++ b/sprue/pkg/service/handlers/upload_shard_list.go @@ -3,7 +3,7 @@ package handlers import ( "fmt" - shardcmds "github.com/fil-forge/libforge/commands/upload/shard" + shardcmds "github.com/fil-forge/forge/protocol/commands/upload/shard" upload_store "github.com/fil-forge/forge/sprue/pkg/store/upload" "github.com/fil-forge/ucantone/binding" "github.com/fil-forge/ucantone/server" diff --git a/sprue/pkg/service/handlers/upload_shard_list_test.go b/sprue/pkg/service/handlers/upload_shard_list_test.go index 18cfc7ce..b9bd30c6 100644 --- a/sprue/pkg/service/handlers/upload_shard_list_test.go +++ b/sprue/pkg/service/handlers/upload_shard_list_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - shardcmds "github.com/fil-forge/libforge/commands/upload/shard" + shardcmds "github.com/fil-forge/forge/protocol/commands/upload/shard" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/service/handlers" upload_store "github.com/fil-forge/forge/sprue/pkg/store/upload/memory" diff --git a/sprue/pkg/service/service.go b/sprue/pkg/service/service.go index 2d531ded..6ce1fd02 100644 --- a/sprue/pkg/service/service.go +++ b/sprue/pkg/service/service.go @@ -8,9 +8,9 @@ import ( "slices" "time" - "github.com/fil-forge/libforge/attestation" - "github.com/fil-forge/libforge/attestation/didmailto" - "github.com/fil-forge/libforge/identity" + "github.com/fil-forge/forge/attestation" + "github.com/fil-forge/forge/attestation/didmailto" + "github.com/fil-forge/forge/internal/identity" "github.com/fil-forge/forge/sprue/pkg/lib/ucan_server" "github.com/fil-forge/forge/sprue/pkg/service/ui" "github.com/fil-forge/forge/sprue/pkg/store/agent" diff --git a/sprue/pkg/store/agent/agent_test.go b/sprue/pkg/store/agent/agent_test.go index 32db418d..4d244d24 100644 --- a/sprue/pkg/store/agent/agent_test.go +++ b/sprue/pkg/store/agent/agent_test.go @@ -5,7 +5,7 @@ import ( "runtime" "testing" - ucancap "github.com/fil-forge/libforge/commands/ucan" + ucancap "github.com/fil-forge/forge/protocol/commands/ucan" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/store/agent" agentmemory "github.com/fil-forge/forge/sprue/pkg/store/agent/memory" diff --git a/sprue/pkg/store/blob_registry/blob_registry.go b/sprue/pkg/store/blob_registry/blob_registry.go index d0d0c816..98792ffc 100644 --- a/sprue/pkg/store/blob_registry/blob_registry.go +++ b/sprue/pkg/store/blob_registry/blob_registry.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/fil-forge/libforge/commands/blob" + "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/forge/sprue/pkg/store" "github.com/fil-forge/ucantone/did" "github.com/fil-forge/ucantone/errors" diff --git a/sprue/pkg/store/blob_registry/blob_registry_test.go b/sprue/pkg/store/blob_registry/blob_registry_test.go index 9c5a464d..c135d341 100644 --- a/sprue/pkg/store/blob_registry/blob_registry_test.go +++ b/sprue/pkg/store/blob_registry/blob_registry_test.go @@ -5,7 +5,7 @@ import ( "runtime" "testing" - "github.com/fil-forge/libforge/commands/blob" + "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/forge/sprue/internal/testutil" "github.com/fil-forge/forge/sprue/pkg/store" blobregistry "github.com/fil-forge/forge/sprue/pkg/store/blob_registry" @@ -84,7 +84,7 @@ func createPostgresStores(t *testing.T) storeBundle { // randomBlob returns a blob with a random digest and the given size. func randomBlob(t *testing.T, size uint64) blob.Blob { t.Helper() - return blob.Blob{Digest: testutil.RandomMultihash(t), Size: size} + return blob.Blob{Digest: testutil.RandomDigest(t), Size: size} } func TestBlobRegistryStore(t *testing.T) { @@ -151,7 +151,7 @@ func TestBlobRegistryStore(t *testing.T) { b := makeStores(t, k) space := testutil.RandomDID(t) - _, err := b.registry.Get(t.Context(), space, testutil.RandomMultihash(t)) + _, err := b.registry.Get(t.Context(), space, testutil.RandomDigest(t)) require.ErrorIs(t, err, blobregistry.ErrEntryNotFound) }) @@ -193,7 +193,7 @@ func TestBlobRegistryStore(t *testing.T) { b := makeStores(t, k) space := testutil.RandomDID(t) - err := b.registry.Deregister(t.Context(), space, testutil.RandomMultihash(t), testutil.RandomCID(t)) + err := b.registry.Deregister(t.Context(), space, testutil.RandomDigest(t), testutil.RandomCID(t)) require.ErrorIs(t, err, blobregistry.ErrEntryNotFound) }) diff --git a/sprue/pkg/store/blob_registry/memory/store.go b/sprue/pkg/store/blob_registry/memory/store.go index 04365795..9a5d07e2 100644 --- a/sprue/pkg/store/blob_registry/memory/store.go +++ b/sprue/pkg/store/blob_registry/memory/store.go @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/fil-forge/libforge/commands/blob" + "github.com/fil-forge/forge/protocol/commands/blob" "github.com/fil-forge/forge/sprue/pkg/store" blobregistry "github.com/fil-forge/forge/sprue/pkg/store/blob_registry" "github.com/fil-forge/forge/sprue/pkg/store/consumer" diff --git a/sprue/pkg/store/blob_registry/postgres/store.go b/sprue/pkg/store/blob_registry/postgres/store.go index 91f31675..bbfe29ee 100644 --- a/sprue/pkg/store/blob_registry/postgres/store.go +++ b/sprue/pkg/store/blob_registry/postgres/store.go @@ -9,7 +9,7 @@ import ( "fmt" "time" - "github.com/fil-forge/libforge/digestutil" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/forge/sprue/pkg/store" blobregistry "github.com/fil-forge/forge/sprue/pkg/store/blob_registry" "github.com/fil-forge/forge/sprue/pkg/store/consumer" diff --git a/sprue/pkg/store/metrics/metrics.go b/sprue/pkg/store/metrics/metrics.go index e5f6a003..001e14a0 100644 --- a/sprue/pkg/store/metrics/metrics.go +++ b/sprue/pkg/store/metrics/metrics.go @@ -3,8 +3,8 @@ package metrics import ( "context" - "github.com/fil-forge/libforge/commands/blob" - "github.com/fil-forge/libforge/commands/upload" + "github.com/fil-forge/forge/protocol/commands/blob" + "github.com/fil-forge/forge/protocol/commands/upload" "github.com/fil-forge/ucantone/did" ) diff --git a/sprue/pkg/store/replica/memory/store.go b/sprue/pkg/store/replica/memory/store.go index 7b6e3c88..993b3fba 100644 --- a/sprue/pkg/store/replica/memory/store.go +++ b/sprue/pkg/store/replica/memory/store.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/fil-forge/libforge/bytemap" + "github.com/fil-forge/forge/protocol/bytemap" "github.com/fil-forge/forge/sprue/pkg/store/replica" "github.com/fil-forge/ucantone/did" cid "github.com/ipfs/go-cid" diff --git a/sprue/pkg/store/replica/postgres/store.go b/sprue/pkg/store/replica/postgres/store.go index aceed6cc..dee96c57 100644 --- a/sprue/pkg/store/replica/postgres/store.go +++ b/sprue/pkg/store/replica/postgres/store.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "github.com/fil-forge/libforge/digestutil" + "github.com/fil-forge/forge/protocol/digestutil" "github.com/fil-forge/forge/sprue/pkg/store/replica" "github.com/fil-forge/ucantone/did" "github.com/ipfs/go-cid" diff --git a/sprue/pkg/store/replica/replica_test.go b/sprue/pkg/store/replica/replica_test.go index 706caaf0..4dc02ac9 100644 --- a/sprue/pkg/store/replica/replica_test.go +++ b/sprue/pkg/store/replica/replica_test.go @@ -50,7 +50,7 @@ func TestReplicaStore(t *testing.T) { t.Run("adds a replica", func(t *testing.T) { space := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) provider := testutil.RandomDID(t) cause := testutil.RandomCID(t) @@ -70,7 +70,7 @@ func TestReplicaStore(t *testing.T) { t.Run("returns empty list for unknown space", func(t *testing.T) { space := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) records, err := s.List(t.Context(), space, digest) require.NoError(t, err) @@ -79,8 +79,8 @@ func TestReplicaStore(t *testing.T) { t.Run("returns empty list for unknown digest", func(t *testing.T) { space := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) - otherDigest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) + otherDigest := testutil.RandomDigest(t) provider := testutil.RandomDID(t) cause := testutil.RandomCID(t) @@ -94,7 +94,7 @@ func TestReplicaStore(t *testing.T) { t.Run("returns error when adding duplicate replica", func(t *testing.T) { space := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) provider := testutil.RandomDID(t) cause := testutil.RandomCID(t) @@ -107,7 +107,7 @@ func TestReplicaStore(t *testing.T) { t.Run("lists multiple replicas for a blob", func(t *testing.T) { space := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) provider1 := testutil.RandomDID(t) provider2 := testutil.RandomDID(t) cause := testutil.RandomCID(t) @@ -126,7 +126,7 @@ func TestReplicaStore(t *testing.T) { t.Run("isolates replicas by space", func(t *testing.T) { space1 := testutil.RandomDID(t) space2 := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) provider := testutil.RandomDID(t) cause := testutil.RandomCID(t) @@ -139,7 +139,7 @@ func TestReplicaStore(t *testing.T) { t.Run("sets status of a replica", func(t *testing.T) { space := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) provider := testutil.RandomDID(t) cause := testutil.RandomCID(t) @@ -157,7 +157,7 @@ func TestReplicaStore(t *testing.T) { t.Run("returns error when setting status of unknown replica", func(t *testing.T) { space := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) provider := testutil.RandomDID(t) err := s.SetStatus(t.Context(), space, digest, provider, replica.Transferred) @@ -166,7 +166,7 @@ func TestReplicaStore(t *testing.T) { t.Run("retries a replica", func(t *testing.T) { space := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) provider := testutil.RandomDID(t) cause := testutil.RandomCID(t) newCause := testutil.RandomCID(t) @@ -186,7 +186,7 @@ func TestReplicaStore(t *testing.T) { t.Run("returns error when retrying unknown replica", func(t *testing.T) { space := testutil.RandomDID(t) - digest := testutil.RandomMultihash(t) + digest := testutil.RandomDigest(t) provider := testutil.RandomDID(t) cause := testutil.RandomCID(t)