diff --git a/cmd/gateway/serve.go b/cmd/gateway/serve.go index 7bb1fd5f..9a98e6c6 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" @@ -100,6 +101,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) @@ -150,6 +153,13 @@ var serveCmd = &cobra.Command{ } indexer, indexerPrincipal := cmdutil.MustGetIndexClient(cfg.Network) + + network := cmdutil.MustGetNetworkConfig(cfg.Network, "") + 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 { @@ -173,12 +183,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 f72969ba..c89459cf 100644 --- a/cmd/retrieve.go +++ b/cmd/retrieve.go @@ -13,6 +13,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" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" @@ -25,6 +26,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 +85,13 @@ var retrieveCmd = &cobra.Command{ } }() + networkName, _ := cmd.Flags().GetString("network") + network := cmdutil.MustGetNetworkConfig(cfg.Network, networkName) + pruningCtx, err := cmdutil.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,13 +111,31 @@ 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) + } + + prunedPfs, unauth := validator.PruneProofs(ctx, draftDlg, pruningCtx) + if unauth != nil { + return nil, fmt.Errorf("pruning proofs: %w", unauth) + } + return delegation.Delegate( c.Issuer(), indexerPrincipal, []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())), ) }) diff --git a/go.mod b/go.mod index 9faf85e1..62d8ad8d 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 04c5430e..b64d5c10 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.5 h1:zfRbku2RXxbH0uNWnpGQyJqafiJ+uCGs3tMmkHgZ/QE= github.com/storacha/go-libstoracha v0.7.5/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= 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 +}