diff --git a/pkg/client/accessdelegate.go b/pkg/client/accessdelegate.go index 0e749ccd..460933f9 100644 --- a/pkg/client/accessdelegate.go +++ b/pkg/client/accessdelegate.go @@ -5,6 +5,7 @@ import ( "fmt" accesscap "github.com/storacha/go-libstoracha/capabilities/access" + "github.com/storacha/go-ucanto/core/dag/blockstore" "github.com/storacha/go-ucanto/core/delegation" "github.com/storacha/go-ucanto/core/result" "github.com/storacha/go-ucanto/did" @@ -31,10 +32,17 @@ func (c *Client) AccessDelegate(ctx context.Context, space did.DID, delegations }, } + // Include the delegations themselves as proofs // Include the delegations themselves as proofs delOptions := make([]delegation.Option, 0, len(delegations)) for _, del := range delegations { - delOptions = append(delOptions, delegation.WithProof(delegation.FromDelegation(del))) + // Prune the delegation to remove nested proofs (blocks) to reduce header size. + // We use d.Root() because d.Block() does not exist on the interface. + pruned, err := pruneDelegation(del) + if err != nil { + return accesscap.DelegateOk{}, fmt.Errorf("pruning delegation: %w", err) + } + delOptions = append(delOptions, delegation.WithProof(delegation.FromDelegation(pruned))) } res, _, err := invokeAndExecute[accesscap.DelegateCaveats, accesscap.DelegateOk]( @@ -57,3 +65,18 @@ func (c *Client) AccessDelegate(ctx context.Context, space did.DID, delegations return delegateOk, nil } + +// pruneDelegation creates a shallow copy of the delegation that only contains +// the root block in its blockstore. This prevents the entire proof chain (nested blocks) +// from being serialized into the invocation header. +func pruneDelegation(d delegation.Delegation) (delegation.Delegation, error) { + blk := d.Root() + bs, err := blockstore.NewBlockStore() + if err != nil { + return nil, fmt.Errorf("creating blockstore: %w", err) + } + if err := bs.Put(blk); err != nil { + return nil, fmt.Errorf("putting block: %w", err) + } + return delegation.NewDelegation(blk, bs) +} diff --git a/pkg/client/accessdelegate_test.go b/pkg/client/accessdelegate_test.go index e2a5761e..02fd4fff 100644 --- a/pkg/client/accessdelegate_test.go +++ b/pkg/client/accessdelegate_test.go @@ -258,4 +258,81 @@ func TestAccessDelegate(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "access/delegate") }) + + t.Run("prunes nested proofs from delegations", func(t *testing.T) { + root := testutil.Must(signer.Generate())(t) + middle := testutil.Must(signer.Generate())(t) + leaf := testutil.Must(signer.Generate())(t) + + // Root -> Middle + rootToMiddle := testutil.Must(delegation.Delegate( + root, + middle, + []ucan.Capability[ucan.NoCaveats]{ + ucan.NewCapability("store/add", root.DID().String(), ucan.NoCaveats{}), + }, + delegation.WithNoExpiration(), + ))(t) + + // Middle -> Leaf (includes Root->Middle as proof) + // This creates a "heavy" delegation because it carries the history + middleToLeaf := testutil.Must(delegation.Delegate( + middle, + leaf, + []ucan.Capability[ucan.NoCaveats]{ + ucan.NewCapability("store/add", root.DID().String(), ucan.NoCaveats{}), + }, + delegation.WithNoExpiration(), + delegation.WithProof(delegation.FromDelegation(rootToMiddle)), + ))(t) + + var receivedProofLinks []ucan.Link + var c *client.Client + + connection := ctestutil.NewTestServerConnection( + server.WithServiceMethod( + access.DelegateAbility, + server.Provide( + access.Delegate, + func( + ctx context.Context, + cap ucan.Capability[access.DelegateCaveats], + inv invocation.Invocation, + context server.InvocationContext, + ) (result.Result[access.DelegateOk, failure.IPLDBuilderFailure], fx.Effects, error) { + receivedProofLinks = inv.Proofs() + return result.Ok[access.DelegateOk, failure.IPLDBuilderFailure](access.DelegateOk{}), nil, nil + }, + ), + ), + ) + + c = testutil.Must(client.NewClient( + client.WithConnection(connection), + ))(t) + + // Authorize client to call access/delegate + accessDelegateProof := testutil.Must(delegation.Delegate( + root, + c.Issuer(), + []ucan.Capability[ucan.NoCaveats]{ + ucan.NewCapability("access/delegate", root.DID().String(), ucan.NoCaveats{}), + }, + delegation.WithNoExpiration(), + ))(t) + require.NoError(t, c.AddProofs(accessDelegateProof)) + + // Action: Call AccessDelegate with the "heavy" delegation + _, err := c.AccessDelegate(testContext(t), root.DID(), middleToLeaf) + require.NoError(t, err) + + // Verification: Ensure the leaf was sent + var foundLeafProof delegation.Proof + for _, pl := range receivedProofLinks { + if pl.String() == middleToLeaf.Link().String() { + foundLeafProof = delegation.FromDelegation(middleToLeaf) + } + } + require.NotNil(t, foundLeafProof, "expected leaf proof link to be sent") + }) }