Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions agent/agentdata/agentdata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package agentdata

import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"

"github.com/multiformats/go-varint"
"github.com/storacha/go-ucanto/core/delegation"
"github.com/storacha/go-ucanto/principal"
ed25519signer "github.com/storacha/go-ucanto/principal/ed25519/signer"
rsasigner "github.com/storacha/go-ucanto/principal/rsa/signer"
)

type AgentData struct {
Principal principal.Signer
Delegations []delegation.Delegation
}

type agentDataSerialized struct {
Principal []byte
Delegations [][]byte
}

func (ad AgentData) MarshalJSON() ([]byte, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[go hint] I'd use a pointer receiver here, even though we are not mutating it within the method. The general recommendation is to use value receivers until there is a reason to use a pointer receiver. In this case, however, AgentData.Delegations could potentially contain several elements. Using a pointer receiver avoids copying the whole thing on each method call, which is what happens when using value receivers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that would make sense, but unfortunately, it doesn't work. The MarshalJSON API is defined with a value receiver. Switching it to a pointer means it just stops getting marshaled. 😕

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a tricky aspect of Go. Methods that are defined on the value type can be invoked from both values and pointers. OTOH, methods defined on the pointer type can only be accessed from pointers.

This means that if you define MarshalJSON on the value type, both values and pointers implement the Marshaler interface. If it is defined on the pointer type, only pointers will implement the interface. Thus, it will work as expected if what you pass to json.Marshal is a pointer.

In any case, let's leave it as it is because that avoids the pitfall of having to remember that only pointers will get marshaled as expected. I think this is the main reason most implementations use a value receiver for MarshalJSON, and it's a solid one IMHO.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all in all, I'd like to take my original comment back 😄. I was trying to explain the difference between value and pointer receivers but ended up delivering an example of premature optimization.

Besides, there was an error in my reasoning. The concern was that AgentData.Delegations might be big. However, it is a slice, which actually contains a pointer to an underlying array, so the actual memory is not copied even if the slice descriptor is (see https://go.dev/blog/slices-intro if you are interested).

delegations := make([][]byte, 0, len(ad.Delegations))
for _, d := range ad.Delegations {
b, err := io.ReadAll(d.Archive())
if err != nil {
return nil, fmt.Errorf("reading delegation archive: %w", err)
}
delegations = append(delegations, b)
}

return json.Marshal(agentDataSerialized{
Principal: ad.Principal.Encode(),
Delegations: delegations,
})
}

func (ad *AgentData) UnmarshalJSON(b []byte) error {
var s agentDataSerialized
if err := json.Unmarshal(b, &s); err != nil {
return err
}

// Principal

code, err := varint.ReadUvarint(bytes.NewReader(s.Principal))
if err != nil {
return fmt.Errorf("reading private key codec: %s", err)
}

switch code {
case ed25519signer.Code:
ad.Principal, err = ed25519signer.Decode(s.Principal)
if err != nil {
return err
}

case rsasigner.Code:
ad.Principal, err = rsasigner.Decode(s.Principal)
if err != nil {
return err
}

default:
return fmt.Errorf("invalid private key codec: %d", code)
}
Comment on lines +51 to +71
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np: alternatively, we could move this logic to a convenience function in go-ucanto/principal

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we should; I looked for it there when I wrote this. → storacha/go-ucanto#46


// Delegations

ad.Delegations = make([]delegation.Delegation, len(s.Delegations))
for i, db := range s.Delegations {
d, err := delegation.Extract(db)
if err != nil {
return fmt.Errorf("decoding delegation %d: %w", i, err)
}
ad.Delegations[i] = d
}

return nil
}

func (ad AgentData) WriteToFile(path string) error {
b, err := json.Marshal(ad)
if err != nil {
return err
}

return os.WriteFile(path, b, 0600)
}

func ReadFromFile(path string) (AgentData, error) {
b, err := os.ReadFile(path)
if err != nil {
return AgentData{}, err
}

var ad AgentData
json.Unmarshal(b, &ad)
return ad, nil
}
59 changes: 59 additions & 0 deletions agent/agentdata/agentdata_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package agentdata_test

import (
"encoding/json"
"path"
"testing"

"github.com/storacha/go-ucanto/core/delegation"
"github.com/storacha/go-ucanto/principal/ed25519/signer"
"github.com/storacha/guppy/agent/agentdata"
"github.com/stretchr/testify/require"
)

func TestRoundTripAgentData(t *testing.T) {
agentPrincipal, err := signer.Generate()
require.NoError(t, err)

del, err := newDelegation()

require.NoError(t, err)

agentData := agentdata.AgentData{
Principal: agentPrincipal,
Delegations: []delegation.Delegation{del},
}

str, err := json.Marshal(agentData)
require.NoError(t, err)

var agentDataReturned agentdata.AgentData
err = json.Unmarshal(str, &agentDataReturned)
require.NoError(t, err)

require.Equal(t, agentData.Principal, agentDataReturned.Principal)
require.Equal(t, delegationsCids(agentData), delegationsCids(agentDataReturned))
}

func TestWriteReadAgentData(t *testing.T) {
dataFilePath := path.Join(t.TempDir(), "agentdata.json")

agentPrincipal, err := signer.Generate()
require.NoError(t, err)
del, err := newDelegation()
require.NoError(t, err)

agentData := agentdata.AgentData{
Principal: agentPrincipal,
Delegations: []delegation.Delegation{del},
}

err = agentData.WriteToFile(dataFilePath)
require.NoError(t, err)

agentDataReturned, err := agentdata.ReadFromFile(dataFilePath)
require.NoError(t, err)

require.Equal(t, agentData.Principal, agentDataReturned.Principal)
require.Equal(t, delegationsCids(agentData), delegationsCids(agentDataReturned))
}
57 changes: 57 additions & 0 deletions agent/agentdata/fixture_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package agentdata_test

import (
"crypto/rand"

"github.com/multiformats/go-multihash"
"github.com/storacha/go-libstoracha/capabilities/space/blob"
"github.com/storacha/go-libstoracha/capabilities/types"
"github.com/storacha/go-ucanto/core/delegation"
"github.com/storacha/go-ucanto/core/ipld"
"github.com/storacha/go-ucanto/did"
"github.com/storacha/go-ucanto/principal/ed25519/signer"
"github.com/storacha/guppy/agent/agentdata"
)

func newDelegation() (delegation.Delegation, error) {
signer, err := signer.Generate()
if err != nil {
return nil, err
}

audienceDid, err := did.Parse("did:mailto:example.com:alice")
if err != nil {
return nil, err
}

bytes := make([]byte, 128)
_, err = rand.Read(bytes)
if err != nil {
return nil, err
}

digest, err := multihash.Sum(bytes, multihash.SHA2_256, -1)
if err != nil {
return nil, err
}

return blob.Add.Delegate(
signer,
audienceDid,
signer.DID().String(),
blob.AddCaveats{
Blob: types.Blob{
Digest: digest,
Size: uint64(len(bytes)),
},
},
)
}

func delegationsCids(d agentdata.AgentData) []ipld.Link {
cids := make([]ipld.Link, len(d.Delegations))
for i, d := range d.Delegations {
cids[i] = d.Link()
}
return cids
}
70 changes: 34 additions & 36 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/storacha/guppy

go 1.23.3
go 1.23.8

toolchain go1.24.3

require (
github.com/ipfs/go-cid v0.5.0
Expand All @@ -15,81 +17,77 @@ require (
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/ipfs/boxo v0.30.0 // indirect
github.com/storacha/go-piece v0.0.0-20241102032446-a7c497f05d55 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
)

require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/filecoin-project/go-data-segment v0.0.1 // indirect
github.com/filecoin-project/go-fil-commcid v0.2.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hannahhoward/cbor-gen-for v0.0.0-20230214144701-5d17c9d5243c // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/ipfs/bbloom v0.0.4 // indirect
github.com/ipfs/go-block-format v0.2.0 // indirect
github.com/ipfs/go-block-format v0.2.1 // indirect
github.com/ipfs/go-blockservice v0.5.2 // indirect
github.com/ipfs/go-datastore v0.6.0 // indirect
github.com/ipfs/go-datastore v0.8.2 // indirect
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
github.com/ipfs/go-ipld-cbor v0.1.0 // indirect
github.com/ipfs/go-ipld-format v0.6.0 // indirect
github.com/ipfs/go-ipld-cbor v0.2.0 // indirect
github.com/ipfs/go-ipld-format v0.6.1 // indirect
github.com/ipfs/go-ipld-legacy v0.2.1 // indirect
github.com/ipfs/go-log v1.0.5 // indirect
github.com/ipfs/go-log/v2 v2.5.1 // indirect
github.com/ipfs/go-merkledag v0.11.0 // indirect
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
github.com/ipfs/go-metrics-interface v0.3.0 // indirect
github.com/ipfs/go-verifcid v0.0.3 // indirect
github.com/ipld/go-car v0.6.2 // indirect
github.com/ipld/go-codec-dagpb v1.6.0 // indirect
github.com/jbenet/goprocess v0.1.4 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/ipld/go-codec-dagpb v1.7.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.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 v0.14.0 // indirect
github.com/multiformats/go-multiaddr v0.15.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.23.3 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/ice/v2 v2.3.37 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/sctp v1.8.37 // indirect
github.com/pion/stun v0.6.1 // indirect
github.com/pion/webrtc/v4 v4.0.14 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polydawn/refmt v0.89.1-0.20231129105047-37766d95467a // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/storacha/go-capabilities v0.0.0-20250604155856-9e5efefb7de0
github.com/ucan-wg/go-ucan v0.0.0-20240916120445-37f52863156c // indirect
github.com/whyrusleeping/cbor-gen v0.2.0 // indirect
github.com/whyrusleeping/cbor-gen v0.3.1
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
go.opentelemetry.io/otel/metric v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.30.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20250128182459-e0ece0dbea4c // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/protobuf v1.36.5 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/tools v0.32.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
lukechampine.com/blake3 v1.4.0 // indirect
)

tool github.com/hannahhoward/cbor-gen-for
Loading
Loading