From 08e6b579f7045f9829500c0af7b33e9134b33f2b Mon Sep 17 00:00:00 2001 From: Vicente Olmedo Date: Wed, 25 Feb 2026 12:08:03 +0100 Subject: [PATCH 1/2] fix: only send necessary proofs in retrieval auth delegation --- cmd/retrieve.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 3 ++ go.sum | 4 +-- 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/cmd/retrieve.go b/cmd/retrieve.go index c0ad0c40..03b89198 100644 --- a/cmd/retrieve.go +++ b/cmd/retrieve.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "io" "io/fs" @@ -10,9 +11,14 @@ import ( "github.com/mitchellh/go-wordwrap" "github.com/spf13/cobra" contentcap "github.com/storacha/go-libstoracha/capabilities/space/content" + "github.com/storacha/go-libstoracha/principalresolver" "github.com/storacha/go-ucanto/core/delegation" "github.com/storacha/go-ucanto/did" + edverifier "github.com/storacha/go-ucanto/principal/ed25519/verifier" + "github.com/storacha/go-ucanto/principal/verifier" + "github.com/storacha/go-ucanto/server" "github.com/storacha/go-ucanto/ucan" + "github.com/storacha/go-ucanto/validator" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" @@ -25,6 +31,11 @@ import ( "github.com/storacha/guppy/pkg/dagfs" ) +func init() { + retrieveCmd.Flags().StringP("network", "n", "", "Network to retrieve content from.") + retrieveCmd.Flags().MarkHidden("network") +} + var retrieveCmd = &cobra.Command{ Use: "retrieve ", Aliases: []string{"get"}, @@ -79,6 +90,13 @@ var retrieveCmd = &cobra.Command{ } }() + networkName, _ := cmd.Flags().GetString("network") + network := cmdutil.MustGetNetworkConfig(networkName) + pruningCtx, err := buildPruningContext(ctx, network.UploadID) + if err != nil { + return err + } + locator := locator.NewIndexLocator(indexer, func(spaces []did.DID) (delegation.Delegation, error) { queries := make([]agentstore.CapabilityQuery, 0, len(spaces)) for _, space := range spaces { @@ -98,6 +116,24 @@ var retrieveCmd = &cobra.Command{ } // Allow the indexing service to retrieve indexes + // Prune proofs from the delegation to avoid sending large delegations to the indexer + draftDlg, err := contentcap.Retrieve.Delegate( + c.Issuer(), + indexerPrincipal, + space.DID().String(), + contentcap.RetrieveCaveats{}, + delegation.WithProof(pfs...), + delegation.WithExpiration(int(time.Now().Add(30*time.Second).Unix())), + ) + if err != nil { + return nil, fmt.Errorf("creating delegation to indexer: %w", err) + } + + pfs, unauth := validator.PruneProofs(ctx, draftDlg, pruningCtx) + if unauth != nil { + return nil, unauth + } + return delegation.Delegate( c.Issuer(), indexerPrincipal, @@ -151,3 +187,40 @@ var retrieveCmd = &cobra.Command{ return nil }, } + +func buildPruningContext(ctx context.Context, attestorDID did.DID) (validator.ValidationContext[contentcap.RetrieveCaveats], error) { + resolver, err := principalresolver.NewHTTPResolver([]did.DID{attestorDID}) + if err != nil { + return nil, fmt.Errorf("creating principal resolver: %w", err) + } + + resolvedKeyDID, unresolvedErr := resolver.ResolveDIDKey(ctx, attestorDID) + if unresolvedErr != nil { + return nil, fmt.Errorf("resolving attestor DID key: %w", unresolvedErr) + } + + keyVerifier, err := edverifier.Parse(resolvedKeyDID.String()) + if err != nil { + return nil, fmt.Errorf("parsing resolved key DID: %w", err) + } + + attestor, err := verifier.Wrap(keyVerifier, attestorDID) + if err != nil { + return nil, fmt.Errorf("creating attestor verifier: %w", err) + } + + return validator.NewValidationContext( + // For client evaluated chains, the authority must be the service that issued the ucan/attest + // delegations — e.g. the upload-service. + attestor, + contentcap.Retrieve, + validator.IsSelfIssued, + func(context.Context, validator.Authorization[any]) validator.Revoked { + return nil + }, + validator.ProofUnavailable, + server.ParsePrincipal, + validator.FailDIDKeyResolution, + validator.NotExpiredNotTooEarly, + ), nil +} diff --git a/go.mod b/go.mod index 5f6875ec..f4e03ff2 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/storacha/guppy go 1.25.3 +replace github.com/storacha/go-ucanto => ../go-ucanto + require ( github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef github.com/briandowns/spinner v1.23.2 @@ -115,6 +117,7 @@ require ( github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect github.com/multiformats/go-multistream v0.6.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/paulmach/orb v0.11.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect diff --git a/go.sum b/go.sum index 19225cf2..3b7efef4 100644 --- a/go.sum +++ b/go.sum @@ -640,6 +640,8 @@ github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/ 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/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +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/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= @@ -794,8 +796,6 @@ github.com/storacha/go-fil-commp-hashhash v0.0.0-20251204184521-dc48123eb846 h1: github.com/storacha/go-fil-commp-hashhash v0.0.0-20251204184521-dc48123eb846/go.mod h1:YbWvDVjuho1gx+xQNrbM95NNWnJ4oAy067mY5t5F0Bw= github.com/storacha/go-libstoracha v0.7.3 h1:4X9XI0djmURhhabakvmRUy2NUq40Z04ns6NLTY8TmQ8= github.com/storacha/go-libstoracha v0.7.3/go.mod h1:htUh/VZ0qHRLPJKWZsgXv9mCOqlAFGTVS//ApvQVNf0= -github.com/storacha/go-ucanto v0.7.3-0.20260217160605-b36b2fa2b6fc h1:zojq5HkvwbPGFpPQ4QokEJlScNLZlOc4fkj/QVigYrM= -github.com/storacha/go-ucanto v0.7.3-0.20260217160605-b36b2fa2b6fc/go.mod h1:DZlWyzuSkXk3phAuJpGDyhxYWpJogW1RFqp/VfldT64= github.com/storacha/indexing-service v1.12.2 h1:DrcIzvM36Ux7i0UmGoSZiU8lR8WjVIqsTULSE1kA+7I= github.com/storacha/indexing-service v1.12.2/go.mod h1:Yk+uHoTA6qaTE13Ptq6FArsR9hESOetzej9194KwjhM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 9e9f2b84b6b37446b03573902d91b519af152fa4 Mon Sep 17 00:00:00 2001 From: Vicente Olmedo Date: Fri, 6 Mar 2026 13:46:58 +0100 Subject: [PATCH 2/2] refactor and prune proofs in gateway serve too --- cmd/gateway/serve.go | 31 ++++++++++++++++++++--- cmd/retrieve.go | 49 +++--------------------------------- internal/cmdutil/proofs.go | 51 +++++++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 49 deletions(-) diff --git a/cmd/gateway/serve.go b/cmd/gateway/serve.go index 22be35c3..92e6969f 100644 --- a/cmd/gateway/serve.go +++ b/cmd/gateway/serve.go @@ -28,6 +28,7 @@ import ( "github.com/storacha/go-ucanto/core/delegation" "github.com/storacha/go-ucanto/did" "github.com/storacha/go-ucanto/ucan" + "github.com/storacha/go-ucanto/validator" "github.com/storacha/guppy/cmd/gateway/banner" "github.com/storacha/guppy/internal/cmdutil" "github.com/storacha/guppy/pkg/agentstore" @@ -97,6 +98,8 @@ var serveCmd = &cobra.Command{ "Spaces can be specified by DID or by name.", 80), RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + cfg, err := config.Load[config.Config]() if err != nil { return fmt.Errorf("loading config: %w", err) @@ -147,6 +150,13 @@ var serveCmd = &cobra.Command{ } indexer, indexerPrincipal := cmdutil.MustGetIndexClient() + + network := cmdutil.MustGetNetworkConfig("") + pruningCtx, err := cmdutil.BuildPruningContext(ctx, network.UploadID) + if err != nil { + return fmt.Errorf("building pruning context: %w", err) + } + locator := locator.NewIndexLocator(indexer, func(spaces []did.DID) (delegation.Delegation, error) { queries := make([]agentstore.CapabilityQuery, 0, len(spaces)) for _, space := range spaces { @@ -170,12 +180,27 @@ var serveCmd = &cobra.Command{ caps = append(caps, ucan.NewCapability(contentcap.RetrieveAbility, space.String(), ucan.NoCaveats{})) } - opts := []delegation.Option{ + // Build a draft delegation covering all caps to determine the minimum + // needed proofs, then prune before sending to avoid large HTTP headers. + draftDlg, err := delegation.Delegate( + c.Issuer(), indexerPrincipal, caps, delegation.WithProof(pfs...), - delegation.WithExpiration(int(time.Now().Add(30 * time.Second).Unix())), + delegation.WithExpiration(int(time.Now().Add(30*time.Second).Unix())), + ) + if err != nil { + return nil, fmt.Errorf("creating draft delegation: %w", err) + } + + prunedPfs, unauth := validator.PruneProofs(ctx, draftDlg, pruningCtx) + if unauth != nil { + return nil, fmt.Errorf("pruning proofs: %w", unauth) } - return delegation.Delegate(c.Issuer(), indexerPrincipal, caps, opts...) + return delegation.Delegate( + c.Issuer(), indexerPrincipal, caps, + delegation.WithProof(prunedPfs...), + delegation.WithExpiration(int(time.Now().Add(30*time.Second).Unix())), + ) }) exchange := dagservice.NewExchange(locator, c, spaces) diff --git a/cmd/retrieve.go b/cmd/retrieve.go index 03b89198..baf5331b 100644 --- a/cmd/retrieve.go +++ b/cmd/retrieve.go @@ -1,7 +1,6 @@ package cmd import ( - "context" "fmt" "io" "io/fs" @@ -11,12 +10,8 @@ import ( "github.com/mitchellh/go-wordwrap" "github.com/spf13/cobra" contentcap "github.com/storacha/go-libstoracha/capabilities/space/content" - "github.com/storacha/go-libstoracha/principalresolver" "github.com/storacha/go-ucanto/core/delegation" "github.com/storacha/go-ucanto/did" - edverifier "github.com/storacha/go-ucanto/principal/ed25519/verifier" - "github.com/storacha/go-ucanto/principal/verifier" - "github.com/storacha/go-ucanto/server" "github.com/storacha/go-ucanto/ucan" "github.com/storacha/go-ucanto/validator" "go.opentelemetry.io/otel/attribute" @@ -92,7 +87,7 @@ var retrieveCmd = &cobra.Command{ networkName, _ := cmd.Flags().GetString("network") network := cmdutil.MustGetNetworkConfig(networkName) - pruningCtx, err := buildPruningContext(ctx, network.UploadID) + pruningCtx, err := cmdutil.BuildPruningContext(ctx, network.UploadID) if err != nil { return err } @@ -129,9 +124,9 @@ var retrieveCmd = &cobra.Command{ return nil, fmt.Errorf("creating delegation to indexer: %w", err) } - pfs, unauth := validator.PruneProofs(ctx, draftDlg, pruningCtx) + prunedPfs, unauth := validator.PruneProofs(ctx, draftDlg, pruningCtx) if unauth != nil { - return nil, unauth + return nil, fmt.Errorf("pruning proofs: %w", unauth) } return delegation.Delegate( @@ -140,7 +135,7 @@ var retrieveCmd = &cobra.Command{ []ucan.Capability[ucan.NoCaveats]{ ucan.NewCapability(contentcap.Retrieve.Can(), space.DID().String(), ucan.NoCaveats{}), }, - delegation.WithProof(pfs...), + delegation.WithProof(prunedPfs...), delegation.WithExpiration(int(time.Now().Add(30*time.Second).Unix())), ) }) @@ -188,39 +183,3 @@ var retrieveCmd = &cobra.Command{ }, } -func buildPruningContext(ctx context.Context, attestorDID did.DID) (validator.ValidationContext[contentcap.RetrieveCaveats], error) { - resolver, err := principalresolver.NewHTTPResolver([]did.DID{attestorDID}) - if err != nil { - return nil, fmt.Errorf("creating principal resolver: %w", err) - } - - resolvedKeyDID, unresolvedErr := resolver.ResolveDIDKey(ctx, attestorDID) - if unresolvedErr != nil { - return nil, fmt.Errorf("resolving attestor DID key: %w", unresolvedErr) - } - - keyVerifier, err := edverifier.Parse(resolvedKeyDID.String()) - if err != nil { - return nil, fmt.Errorf("parsing resolved key DID: %w", err) - } - - attestor, err := verifier.Wrap(keyVerifier, attestorDID) - if err != nil { - return nil, fmt.Errorf("creating attestor verifier: %w", err) - } - - return validator.NewValidationContext( - // For client evaluated chains, the authority must be the service that issued the ucan/attest - // delegations — e.g. the upload-service. - attestor, - contentcap.Retrieve, - validator.IsSelfIssued, - func(context.Context, validator.Authorization[any]) validator.Revoked { - return nil - }, - validator.ProofUnavailable, - server.ParsePrincipal, - validator.FailDIDKeyResolution, - validator.NotExpiredNotTooEarly, - ), nil -} diff --git a/internal/cmdutil/proofs.go b/internal/cmdutil/proofs.go index b9c3a289..c85c1311 100644 --- a/internal/cmdutil/proofs.go +++ b/internal/cmdutil/proofs.go @@ -1,13 +1,22 @@ package cmdutil import ( + "context" + "fmt" + + contentcap "github.com/storacha/go-libstoracha/capabilities/space/content" + "github.com/storacha/go-libstoracha/principalresolver" ucan_bs "github.com/storacha/go-ucanto/core/dag/blockstore" "github.com/storacha/go-ucanto/core/delegation" + "github.com/storacha/go-ucanto/did" + edverifier "github.com/storacha/go-ucanto/principal/ed25519/verifier" + "github.com/storacha/go-ucanto/principal/verifier" + "github.com/storacha/go-ucanto/server" "github.com/storacha/go-ucanto/ucan" "github.com/storacha/go-ucanto/validator" ) -// proofResource finds the resource for a proof, handling the case where the +// ProofResource finds the resource for a proof, handling the case where the // delegated resource is "ucan:*" by recursively checking its proofs to find a // delegation for the specific resource. func ProofResource(proof delegation.Delegation, ability ucan.Ability) (ucan.Resource, bool) { @@ -38,3 +47,43 @@ func ProofResource(proof delegation.Delegation, ability ucan.Ability) (ucan.Reso } return "", false } + +// BuildPruningContext creates a ValidationContext for pruning proofs in +// retrieval auth delegations. The attestorDID should be the upload service DID, +// which is the authority that issues ucan/attest delegations in the proof chain. +func BuildPruningContext(ctx context.Context, attestorDID did.DID) (validator.ValidationContext[contentcap.RetrieveCaveats], error) { + resolver, err := principalresolver.NewHTTPResolver([]did.DID{attestorDID}) + if err != nil { + return nil, fmt.Errorf("creating principal resolver: %w", err) + } + + resolvedKeyDID, err := resolver.ResolveDIDKey(ctx, attestorDID) + if err != nil { + return nil, fmt.Errorf("resolving attestor DID key: %w", err) + } + + keyVerifier, err := edverifier.Parse(resolvedKeyDID.String()) + if err != nil { + return nil, fmt.Errorf("parsing resolved key DID: %w", err) + } + + attestor, err := verifier.Wrap(keyVerifier, attestorDID) + if err != nil { + return nil, fmt.Errorf("creating attestor verifier: %w", err) + } + + return validator.NewValidationContext( + // For client evaluated chains, the authority must be the service that issued the ucan/attest + // delegations — e.g. the upload-service. + attestor, + contentcap.Retrieve, + validator.IsSelfIssued, + func(context.Context, validator.Authorization[any]) validator.Revoked { + return nil + }, + validator.ProofUnavailable, + server.ParsePrincipal, + validator.FailDIDKeyResolution, + validator.NotExpiredNotTooEarly, + ), nil +}