Skip to content
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

Receipt writing #1

Merged
merged 3 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion client/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type ExecutionResponse interface {
}

func Execute(invocations []invocation.Invocation, conn Connection) (ExecutionResponse, error) {
input, err := message.Build(invocations)
input, err := message.Build(invocations, nil)
if err != nil {
return nil, fmt.Errorf("building message: %s", err)
}
Expand Down
18 changes: 18 additions & 0 deletions core/dag/blockstore/blockstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,21 @@ func NewBlockReader(options ...Option) (BlockReader, error) {

return &blockreader{keys, blks}, nil
}

func Encode(view ipld.View, bs BlockWriter) error {
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps this should be a BatchPut operation that BlockWriter implements? This isn't really encoding anything, so I think if not BatchPut then it needs a rename :)

blks := view.Blocks()
for {
b, err := blks.Next()
if err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("reading proof blocks: %s", err)
}
err = bs.Put(b)
if err != nil {
return fmt.Errorf("putting proof block: %s", err)
}
}
return nil
}
25 changes: 5 additions & 20 deletions core/delegation/delegate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package delegation

import (
"fmt"
"io"

"github.com/web3-storage/go-ucanto/core/dag/blockstore"
"github.com/web3-storage/go-ucanto/core/ipld/block"
Expand All @@ -20,7 +19,7 @@ type delegationConfig struct {
nbf uint64
nnc string
fct []ucan.FactBuilder
prf []Delegation
prf Proofs
}

// WithExpiration configures the expiration time in UTC seconds since Unix
Expand Down Expand Up @@ -61,7 +60,7 @@ func WithFacts(fct []ucan.FactBuilder) Option {
// `Delegation` is not the resource owner / service provider, for the delegated
// capabilities, the `proofs` must contain valid `Proof`s containing
// delegations to the `issuer`.
func WithProofs(prf []Delegation) Option {
func WithProofs(prf Proofs) Option {
return func(cfg *delegationConfig) error {
cfg.prf = prf
return nil
Expand All @@ -79,28 +78,14 @@ func Delegate(issuer ucan.Signer, audience ucan.Principal, capabilities []ucan.C
}
}

var links []ucan.Link
bs, err := blockstore.NewBlockStore()
if err != nil {
return nil, err
}

for _, p := range cfg.prf {
links = append(links, p.Link())
blks := p.Blocks()
for {
b, err := blks.Next()
if err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("reading proof blocks: %s", err)
}
err = bs.Put(b)
if err != nil {
return nil, fmt.Errorf("putting proof block: %s", err)
}
}
links, err := cfg.prf.Encode(bs)
if err != nil {
return nil, err
}

data, err := ucan.Issue(
Expand Down
2 changes: 1 addition & 1 deletion core/delegation/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
// Delagation is a materialized view of a UCAN delegation, which can be encoded
// into a UCAN token and used as proof for an invocation or further delegations.
type Delegation interface {
ipld.IPLDView
ipld.View
// Link returns the IPLD link of the root block of the delegation.
Link() ucan.Link
// Archive writes the delegation to a Content Addressed aRchive (CAR).
Expand Down
60 changes: 60 additions & 0 deletions core/delegation/proofs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package delegation

import (
"github.com/web3-storage/go-ucanto/core/dag/blockstore"
"github.com/web3-storage/go-ucanto/core/ipld"
"github.com/web3-storage/go-ucanto/ucan"
)

type Proof struct {
delegation Delegation
link ucan.Link
}

func (p Proof) Delegation() (Delegation, bool) {
return p.delegation, p.delegation != nil
}

func (p Proof) Link() ucan.Link {
if p.delegation != nil {
return p.delegation.Link()
}
return p.link
}

func FromDelegation(delegation Delegation) Proof {
return Proof{delegation, nil}
}

func FromLink(link ucan.Link) Proof {
return Proof{nil, link}
}

type Proofs []Proof

func NewProofsView(links []ipld.Link, bs blockstore.BlockReader) Proofs {
proofs := make(Proofs, 0, len(links))
for _, link := range links {
if delegation, err := NewDelegationView(link, bs); err == nil {
proofs = append(proofs, FromDelegation(delegation))
} else {
proofs = append(proofs, FromLink(link))
}
}
return proofs
}

// Encode writes a set of proofs, some of which may be full delegations to a blockstore
func (proofs Proofs) Encode(bs blockstore.BlockWriter) ([]ipld.Link, error) {
Copy link
Member

@alanshaw alanshaw Jul 29, 2024

Choose a reason for hiding this comment

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

"Encode" to me is a function that transforms some data structure into another. This function is more like a utility to write a set of blocks into a blockstore as a convenience (I appreciate each block gets encoded as side effect). I'd perhaps rename to WriteInto? We have similar method naming in JS ucanto already, although it's never an instance method...

That said, it's a little confusing that this may or may not write blocks to a blockstore, depending on whether the instance is backed by an actual delegation. I can imagine it causing bugs that may be tricky to track down.

I propose removing this function and instead move the logic to where it is used, so it can be seen to be explicitly handled, I don't think there are multiple usages anyway(?)

Copy link
Member Author

Choose a reason for hiding this comment

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

so it's worth noting that all over the JS code this "maybe present" for proofs exists -- also for the invocatin in the receipt. I'd say see if you need it while working on the validator / server.

Copy link
Member Author

Choose a reason for hiding this comment

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

note that this is actually present in the JS delegation as well as receipt. I don't quite know what that implies, but it seems the goal is to be able to construct receipts and invocations without every block for every proof.

links := make([]ucan.Link, 0, len(proofs))
for _, p := range proofs {
links = append(links, p.Link())
if delegation, isDelegation := p.Delegation(); isDelegation {
err := blockstore.Encode(delegation, bs)
if err != nil {
return nil, err
}
}
}
return links, nil
}
31 changes: 31 additions & 0 deletions core/invocation/invocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,34 @@ type IssuedInvocation interface {
func Invoke(issuer ucan.Signer, audience ucan.Principal, capability ucan.Capability[ucan.CaveatBuilder], options ...delegation.Option) (IssuedInvocation, error) {
return delegation.Delegate(issuer, audience, []ucan.Capability[ucan.CaveatBuilder]{capability}, options...)
}

type Ran struct {
invocation Invocation
link ucan.Link
}

func (r Ran) Invocation() (Invocation, bool) {
return r.invocation, r.invocation != nil
}

func (r Ran) Link() ucan.Link {
if r.invocation != nil {
return r.invocation.Link()
}
return r.link
}

func FromInvocation(invocation Invocation) Ran {
return Ran{invocation, nil}
}

func FromLink(link ucan.Link) Ran {
return Ran{nil, link}
}

func (r Ran) Encode(bs blockstore.BlockWriter) (ipld.Link, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Similar to my previous comment, this is a "maybe write to blockstore" function - I'd move to where it is used or rename and heavily document.

if invocation, ok := r.Invocation(); ok {
return r.Link(), blockstore.Encode(invocation, bs)
}
return r.Link(), nil
}
1 change: 1 addition & 0 deletions core/ipld/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ import (

type Link = ipld.Link
type Block = block.Block
type Node = ipld.Node
13 changes: 12 additions & 1 deletion core/ipld/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
// View represents a materialized IPLD DAG View, which provides a generic
// traversal API. It is useful for encoding (potentially partial) IPLD DAGs
// into content archives (e.g. CARs).
type IPLDView interface {
type View interface {
Copy link
Member

Choose a reason for hiding this comment

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

👍

// Root is the root block of the IPLD DAG this is the view of. This is the
// block from which all other blocks are linked directly or transitively.
Root() Block
Expand All @@ -21,3 +21,14 @@ type IPLDView interface {
// omitting it when encoding the view into a CAR archive.
Blocks() iterable.Iterator[Block]
}

// ViewBuilder represents a materializable IPLD DAG View. It is a useful
// abstraction that can be used to defer actual IPLD encoding.
//
// Note that represented DAG could be partial implying that some of the blocks
// may not be included. This by design allowing a user to include whatever
// blocks they want to include.
type ViewBuilder[V View] interface {
// BuildIPLDView encodes all the blocks and creates a new IPLDView instance over them.
BuildIPLDView() V
}
36 changes: 23 additions & 13 deletions core/message/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package message

import (
"fmt"
"io"

"github.com/web3-storage/go-ucanto/core/dag/blockstore"
"github.com/web3-storage/go-ucanto/core/invocation"
Expand All @@ -12,10 +11,11 @@ import (
"github.com/web3-storage/go-ucanto/core/ipld/hash/sha256"
"github.com/web3-storage/go-ucanto/core/iterable"
mdm "github.com/web3-storage/go-ucanto/core/message/datamodel"
"github.com/web3-storage/go-ucanto/core/receipt"
)

type AgentMessage interface {
ipld.IPLDView
ipld.View
// Invocations is a list of links to the root block of invocations than can
// be found in the message.
Invocations() []ipld.Link
Expand Down Expand Up @@ -73,7 +73,7 @@ func (m *message) Get(link ipld.Link) (ipld.Link, bool) {
return rcpt, true
}

func Build(invocations []invocation.Invocation) (AgentMessage, error) {
func Build(invocations []invocation.Invocation, receipts []receipt.UniversalReceipt) (AgentMessage, error) {
bs, err := blockstore.NewBlockStore()
if err != nil {
return nil, err
Expand All @@ -83,25 +83,35 @@ func Build(invocations []invocation.Invocation) (AgentMessage, error) {
for _, inv := range invocations {
ex = append(ex, inv.Link())

blks := inv.Blocks()
for {
b, err := blks.Next()
err := blockstore.Encode(inv, bs)
if err != nil {
return nil, err
}
}

var report *mdm.ReportModel
if len(receipts) > 0 {
report = &mdm.ReportModel{
Keys: make([]string, 0, len(receipts)),
Values: make(map[string]ipld.Link, len(receipts)),
}
for _, receipt := range receipts {
err := blockstore.Encode(receipt, bs)
if err != nil {
if err == io.EOF {
break
}
return nil, fmt.Errorf("reading invocation blocks: %s", err)
return nil, err
}
err = bs.Put(b)
if err != nil {
return nil, fmt.Errorf("putting invocation block: %s", err)

key := receipt.Ran().Link().String()
if _, ok := report.Values[key]; !ok {
report.Values[key] = receipt.Root().Link()
}
}
}

msg := mdm.AgentMessageModel{
UcantoMessage7: &mdm.DataModel{
Execute: ex,
Report: report,
},
}

Expand Down
4 changes: 2 additions & 2 deletions core/receipt/datamodel/receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ type MetaModel struct {
}

type ResultModel[O any, X any] struct {
Ok O
Err X
Ok *O
Err *X
Copy link
Member

Choose a reason for hiding this comment

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

Could you explain why this is needed please?

Copy link
Member Author

Choose a reason for hiding this comment

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

So, a union type per bindnode encoding is ALWAYS a pointer type. But the point just encodes whether or not it's present, which is then used to generate the keyed union. You'll note over in go-w3up you're forced to pass pointer types to the receipt. In fact, if you don't the whole code will panic. In my opinion, since no receipt that doesn't have a pointer type will EVER decode or encode properly, it perhaps makes better sense to hide the pointer requirement, which is IPLD specific, from the user.

}

// NewReceiptModelType creates a new schema.Type for a Receipt. You must
Expand Down
16 changes: 8 additions & 8 deletions core/receipt/datamodel/receipt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ func TestEncodeDecode(t *testing.T) {
}

l := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")}
r0 := rdm.ReceiptModel[*resultOk, *resultErr]{
Ocm: rdm.OutcomeModel[*resultOk, *resultErr]{
r0 := rdm.ReceiptModel[resultOk, resultErr]{
Ocm: rdm.OutcomeModel[resultOk, resultErr]{
Ran: l,
Out: rdm.ResultModel[*resultOk, *resultErr]{
Out: rdm.ResultModel[resultOk, resultErr]{
Ok: &resultOk{Status: "done"},
},
},
Expand All @@ -51,7 +51,7 @@ func TestEncodeDecode(t *testing.T) {
if err != nil {
t.Fatalf("encoding receipt: %s", err)
}
r1 := rdm.ReceiptModel[*resultOk, *resultErr]{}
r1 := rdm.ReceiptModel[resultOk, resultErr]{}
err = block.Decode(b0, &r1, typ, cbor.Codec, sha256.Hasher)
if err != nil {
t.Fatalf("decoding receipt: %s", err)
Expand All @@ -63,10 +63,10 @@ func TestEncodeDecode(t *testing.T) {
t.Fatalf("status was not done")
}

r2 := rdm.ReceiptModel[*resultOk, *resultErr]{
Ocm: rdm.OutcomeModel[*resultOk, *resultErr]{
r2 := rdm.ReceiptModel[resultOk, resultErr]{
Ocm: rdm.OutcomeModel[resultOk, resultErr]{
Ran: l,
Out: rdm.ResultModel[*resultOk, *resultErr]{
Out: rdm.ResultModel[resultOk, resultErr]{
Err: &resultErr{Message: "boom"},
},
},
Expand All @@ -75,7 +75,7 @@ func TestEncodeDecode(t *testing.T) {
if err != nil {
t.Fatalf("encoding receipt: %s", err)
}
r3 := rdm.ReceiptModel[*resultOk, *resultErr]{}
r3 := rdm.ReceiptModel[resultOk, resultErr]{}
err = block.Decode(b1, &r3, typ, cbor.Codec, sha256.Hasher)
if err != nil {
t.Fatalf("decoding receipt: %s", err)
Expand Down
Loading
Loading