-
Notifications
You must be signed in to change notification settings - Fork 18
AgentData exists and can persist to filesystem
#21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ab66e34
7215e2d
df2f902
827eeb2
b428ae4
5e0c319
52f3f48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) { | ||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. np: alternatively, we could move this logic to a convenience function in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
| 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)) | ||
| } |
| 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 | ||
| } |
There was a problem hiding this comment.
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.Delegationscould 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.There was a problem hiding this comment.
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
MarshalJSONAPI is defined with a value receiver. Switching it to a pointer means it just stops getting marshaled. 😕There was a problem hiding this comment.
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
MarshalJSONon the value type, both values and pointers implement theMarshalerinterface. If it is defined on the pointer type, only pointers will implement the interface. Thus, it will work as expected if what you pass tojson.Marshalis 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.There was a problem hiding this comment.
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.Delegationsmight 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).