Skip to content

Commit 7ca249a

Browse files
committed
wip: ucan1 integration snapshot for smelt e2e
Snapshots a working end-to-end upload flow through the smelt local-dev stack. See smelt/docs/HANDOFF.md for full context.
1 parent b1cbcd1 commit 7ca249a

12 files changed

Lines changed: 238 additions & 91 deletions

File tree

cmd/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ func initConfig() {
114114
}
115115

116116
func initLogging() {
117+
logLevel = "info"
117118
if logLevel != "" {
118119
ll, err := logging.LevelFromString(logLevel)
119120
cobra.CheckErr(err)

cmd/cli/setup/register.go

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"github.com/fil-forge/ucantone/did"
1919
"github.com/fil-forge/ucantone/principal"
2020
"github.com/fil-forge/ucantone/ucan/container"
21-
"github.com/fil-forge/ucantone/ucan/delegation"
2221
logging "github.com/ipfs/go-log/v2"
2322
"github.com/samber/lo"
2423
"github.com/spf13/cobra"
@@ -820,51 +819,42 @@ func registerWithDelegator(ctx context.Context, cmd *cobra.Command, cfg *appcfg.
820819
return "", "", fmt.Errorf("missing proofs from delegator")
821820
}
822821

823-
indexerProof, err := extractDelegationFromContainer(res.Proofs.Indexer)
822+
indexerProof, err := encodeProofChain(res.Proofs.Indexer)
824823
if err != nil {
825-
return "", "", fmt.Errorf("extracting indexer delegation: %w", err)
824+
return "", "", fmt.Errorf("encoding indexer proof chain: %w", err)
826825
}
827-
egressTrackerProof, err := extractDelegationFromContainer(res.Proofs.EgressTracker)
826+
egressTrackerProof, err := encodeProofChain(res.Proofs.EgressTracker)
828827
if err != nil {
829-
return "", "", fmt.Errorf("extracting egress tracker delegation: %w", err)
828+
return "", "", fmt.Errorf("encoding egress tracker proof chain: %w", err)
830829
}
831830

832831
cmd.PrintErrln("✅ Received proofs from delegator")
833832

834833
return indexerProof, egressTrackerProof, nil
835834
}
836835

837-
// extractDelegationFromContainer decodes a gzipped ucantone container and
838-
// returns the *leaf* delegation's CBOR bytes (the last entry — delegator →
839-
// storage node), encoded as plain CBOR.
836+
// encodeProofChain takes the gzipped ucantone container returned by the
837+
// delegator (which carries a chain: root proof e.g. indexing → delegator, then
838+
// leaf delegator → storage node) and re-encodes the whole container as a
839+
// base64 string suitable for TOML storage. Piri needs every link in the chain
840+
// when invoking /claim/cache (or /space/egress/track) so that the indexing /
841+
// egress-tracker service can validate the operator's authority back through
842+
// the delegator. The consumer (pkg/config/services.go's decodeProofChain)
843+
// base64-decodes and runs container.Decode to recover the chain.
840844
//
841-
// TODO(ucan1, chain): the container actually carries a chain — the root proof
842-
// (e.g. indexing → delegator) followed by the leaf (delegator → storage
843-
// node). See delegator/internal/services/registrar/delegator.go
844-
// generateIndexerDelegation. Piri needs the whole chain to invoke /claim/cache
845-
// successfully, but the downstream consumer (pkg/config/services.go) is still
846-
// typed as `ucan.Delegation` (singular). Returning the leaf only is enough
847-
// to make registration unblock; once services.go is migrated to accept a
848-
// container or []ucan.Delegation, return the encoded container instead.
849-
func extractDelegationFromContainer(b []byte) (string, error) {
850-
ct, err := container.Decode(b)
845+
// Raw CBOR bytes are not UTF-8 safe and break TOML round-trip with
846+
// "invalid character U+..." — base64 keeps the payload TOML-safe.
847+
func encodeProofChain(wire []byte) (string, error) {
848+
// Sanity check: ensure the wire bytes are decodable so we fail at init
849+
// time rather than at first invocation.
850+
ct, err := container.Decode(wire)
851851
if err != nil {
852852
return "", fmt.Errorf("decoding container: %w", err)
853853
}
854-
dlgs := ct.Delegations()
855-
if len(dlgs) == 0 {
854+
if len(ct.Delegations()) == 0 {
856855
return "", fmt.Errorf("no delegations in container")
857856
}
858-
leaf := dlgs[len(dlgs)-1]
859-
encoded, err := delegation.Encode(leaf)
860-
if err != nil {
861-
return "", fmt.Errorf("encoding delegation: %w", err)
862-
}
863-
// CBOR is raw binary, not UTF-8 — putting it directly into a TOML string
864-
// triggers a parse error on read ("invalid character U+..."). Base64 keeps
865-
// the proof TOML-safe. The consumer (pkg/config/services.go) base64-decodes
866-
// before calling delegation.Decode.
867-
return base64.StdEncoding.EncodeToString(encoded), nil
857+
return base64.StdEncoding.EncodeToString(wire), nil
868858
}
869859

870860
func requestContractApproval(ctx context.Context, id principal.Signer, flags *initFlags, ownerAddress common.Address) error {

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ require (
1212
github.com/deckarep/golang-set/v2 v2.6.0
1313
github.com/docker/docker v28.5.1+incompatible
1414
github.com/ethereum/go-ethereum v1.16.7
15-
github.com/fil-forge/delegator v0.0.0-20260526203620-bd1a34ac0f0c
15+
github.com/fil-forge/delegator v0.0.0-20260527145843-e103ee9f563f
1616
github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390
1717
github.com/fil-forge/go-ipni-tools v0.0.0-20260519194815-545b9421aec0
18-
github.com/fil-forge/libforge v0.0.0-20260527084616-1c83212df033
18+
github.com/fil-forge/libforge v0.0.0-20260527182359-ebb22552c348
1919
github.com/fil-forge/piri-signing-service v0.0.0-20260527011208-918512802357
20-
github.com/fil-forge/ucantone v0.0.0-20260522152152-eda937bc2684
20+
github.com/fil-forge/ucantone v0.0.0-20260527115858-517b03bc3c72
2121
github.com/filecoin-project/go-address v1.2.0
2222
github.com/filecoin-project/go-commp-utils v0.1.4
2323
github.com/filecoin-project/go-commp-utils/nonffi v0.0.0-20240802040721-2a04ffc8ffe8

go.sum

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -545,8 +545,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
545545
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
546546
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
547547
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
548-
github.com/fil-forge/delegator v0.0.0-20260526203620-bd1a34ac0f0c h1:tNEZlnoiu/f0tP7QnjwCgyKLdhxzKEe6j/MMsS701p8=
549-
github.com/fil-forge/delegator v0.0.0-20260526203620-bd1a34ac0f0c/go.mod h1:IRZ6E9Vg3Z7UeWMgHUVb94Pn1wELAHKTPTpBigfH+WI=
548+
github.com/fil-forge/delegator v0.0.0-20260527145843-e103ee9f563f h1:ZPyzLZcgT6VIiM/5BGM+JdtB2MaQk3kS+za0pXFrfzo=
549+
github.com/fil-forge/delegator v0.0.0-20260527145843-e103ee9f563f/go.mod h1:IRZ6E9Vg3Z7UeWMgHUVb94Pn1wELAHKTPTpBigfH+WI=
550550
github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 h1:h7SZVboCO1iPDtdxPJjs1fj4IVFq/dpDWFevkXtwgps=
551551
github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390/go.mod h1:ur5fRrG8v9twwwRFdX7lquGza4liT6x/xAFW7T4XVIc=
552552
github.com/fil-forge/go-ipni-tools v0.0.0-20260519194815-545b9421aec0 h1:HAfXUPvtPjK9UAzofbDSzW0mSXsbubMtIFCEh0SJ2ow=
@@ -555,10 +555,14 @@ github.com/fil-forge/go-ucanto v0.0.0-20260507172450-5cb5d073f8ab h1:2J2cDThqTKP
555555
github.com/fil-forge/go-ucanto v0.0.0-20260507172450-5cb5d073f8ab/go.mod h1:lZF3UXZ2hGLKYmXdquG50JqI9pRlUrV6lubGtgOYfwc=
556556
github.com/fil-forge/libforge v0.0.0-20260527084616-1c83212df033 h1:Orw344bJoiOnYYXVkt6sH573hYBl/hpyuNri6CeArNY=
557557
github.com/fil-forge/libforge v0.0.0-20260527084616-1c83212df033/go.mod h1:1ytnrneNEeJcskEbsRDtNZY/Jvgo2Yw5szIUI/9EWPk=
558+
github.com/fil-forge/libforge v0.0.0-20260527182359-ebb22552c348 h1:roYe4llNv0fpQnvXMontrdt/hy9kH5K/cnNsXmhqId4=
559+
github.com/fil-forge/libforge v0.0.0-20260527182359-ebb22552c348/go.mod h1:1ytnrneNEeJcskEbsRDtNZY/Jvgo2Yw5szIUI/9EWPk=
558560
github.com/fil-forge/piri-signing-service v0.0.0-20260527011208-918512802357 h1:sTK8Yc/kds7MkG0cpK5DGDtDCtU1ZwgQWc4OzRf3ZP4=
559561
github.com/fil-forge/piri-signing-service v0.0.0-20260527011208-918512802357/go.mod h1:fgg5hE/BlnFlR5qXsT0POv6GWVu7cj526s7v2+mdJXo=
560562
github.com/fil-forge/ucantone v0.0.0-20260522152152-eda937bc2684 h1:kWJLKVltJXPXO7tKS1z0GhzA+c59gwhediGyByEXE0o=
561563
github.com/fil-forge/ucantone v0.0.0-20260522152152-eda937bc2684/go.mod h1:XAVqsZwYoZ9vncjZoRUAJ+mL/ApLMFn9HHX7ipohVdY=
564+
github.com/fil-forge/ucantone v0.0.0-20260527115858-517b03bc3c72 h1:FFK4CC7IfLfwa5ZQExdkZayPc6Tg0JgVK4jhn2hd2cY=
565+
github.com/fil-forge/ucantone v0.0.0-20260527115858-517b03bc3c72/go.mod h1:xQ1oQ2UgA8xFpNxpyj2v+jiW2KVDAi90xjy1OjUkNXI=
562566
github.com/filecoin-project/filecoin-ffi v1.34.0 h1:OvcsvsFUCwzLOGT949dsJEqSLyGx4d8TPPRrmrzlQbk=
563567
github.com/filecoin-project/filecoin-ffi v1.34.0/go.mod h1:AXLJk1PscWAwEa9CdqdiFwj1ttVJ+UIm8YQDPpTqBjg=
564568
github.com/filecoin-project/go-address v0.0.3/go.mod h1:jr8JxKsYx+lQlQZmF5i2U0Z+cGQ59wMIps/8YW/lDj8=

pkg/config/app/services.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@ type ExternalServicesConfig struct {
1818
}
1919

2020
// IndexingServiceConfig contains indexing service connection and proof(s) for
21-
// using the service
21+
// using the service. Proofs is an ordered chain (root → leaf) — the delegator
22+
// returns a chain (indexing-service → delegator → operator) and every link
23+
// must be attached to invocations to satisfy the indexer's validator.
2224
type IndexingServiceConfig struct {
2325
DID did.DID
2426
Client execution.Executor
25-
Proofs ucan.Delegation
27+
Proofs []ucan.Delegation
2628
}
2729

2830
type EgressTrackerServiceConfig struct {
29-
DID did.DID
30-
Client execution.Executor
31-
Proofs ucan.Delegation
31+
DID did.DID
32+
Client execution.Executor
33+
// Proofs is an ordered chain (root → leaf), same shape as
34+
// IndexingServiceConfig.Proofs.
35+
Proofs []ucan.Delegation
3236
ReceiptsEndpoint *url.URL
3337
MaxBatchSizeBytes int64
3438
CleanupCheckInterval time.Duration

pkg/config/services.go

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,96 @@ import (
88

99
"github.com/fil-forge/ucantone/client"
1010
"github.com/fil-forge/ucantone/did"
11-
"github.com/fil-forge/ucantone/ucan/delegation"
11+
"github.com/fil-forge/ucantone/ucan"
12+
"github.com/fil-forge/ucantone/ucan/container"
1213
"github.com/ipni/go-libipni/maurl"
1314

1415
"github.com/fil-forge/piri/lib"
1516
"github.com/fil-forge/piri/pkg/config/app"
1617
)
1718

18-
// decodeProof base64-decodes a TOML-stored proof string into the raw CBOR
19-
// bytes ready for `delegation.Decode`. The encoder side lives in
20-
// cmd/cli/setup/register.go's extractDelegationFromContainer; both ends MUST
21-
// agree on the encoding. Falls back to treating the string as raw bytes if it
22-
// fails to base64-decode — covers older configs written before the encoding
23-
// was introduced.
24-
func decodeProof(s string) []byte {
25-
if b, err := base64.StdEncoding.DecodeString(s); err == nil {
26-
return b
27-
}
28-
return []byte(s)
19+
// decodeProofChain base64-decodes a TOML-stored proof string into the
20+
// delegation chain it encodes. The encoder side lives in
21+
// cmd/cli/setup/register.go's encodeProofChain; both ends MUST agree on the
22+
// encoding. The chain is logically ordered root → leaf (e.g.
23+
// indexing-service → delegator → operator). All links must travel together
24+
// when the operator invokes against the indexing/egress-tracker services —
25+
// single-delegation storage was insufficient and produced "delegation issuer
26+
// is did:web:indexer not did:web:delegator" errors in piri's publisher when
27+
// only the leaf or only the root made it through.
28+
//
29+
// TODO(forrest)[ucan1]: remove orderProofChain once
30+
// https://github.com/fil-forge/ucantone/issues/29 lands. The ucan-wg/container
31+
// spec sorts tokens bytewise on encode for deterministic output (see
32+
// ucantone/ucan/container/container.go encodeTokens), so ct.Delegations()
33+
// returns them in bytewise order, not in chain order. The ucan-wg/invocation
34+
// spec requires the invocation's `prf` field to be "an array of CIDs ...
35+
// starting from the root Delegation ... in strict sequence where the aud of
36+
// the previous Delegation matches the iss of the next Delegation" — so
37+
// downstream consumers (publisher.go's CacheClaim) cannot just forward
38+
// ct.Delegations() into WithProofs. We reorder here to bridge between the
39+
// transport-layer container and the invocation-layer ordering requirement.
40+
func decodeProofChain(s string) ([]ucan.Delegation, error) {
41+
raw, err := base64.StdEncoding.DecodeString(s)
42+
if err != nil {
43+
return nil, fmt.Errorf("base64-decoding proof: %w", err)
44+
}
45+
ct, err := container.Decode(raw)
46+
if err != nil {
47+
return nil, fmt.Errorf("decoding proof container: %w", err)
48+
}
49+
return orderProofChain(ct.Delegations())
50+
}
51+
52+
// orderProofChain returns dlgs reordered root → leaf so that for each
53+
// adjacent pair (a, b), a.Audience() == b.Issuer(). The root is the
54+
// delegation whose issuer is not the audience of any other delegation in the
55+
// set; from there we walk forward following audience → issuer until the set
56+
// is exhausted. Errors if the chain is disconnected, branched, or cyclic.
57+
func orderProofChain(dlgs []ucan.Delegation) ([]ucan.Delegation, error) {
58+
if len(dlgs) <= 1 {
59+
return dlgs, nil
60+
}
61+
62+
byIssuer := make(map[did.DID]ucan.Delegation, len(dlgs))
63+
audiences := make(map[did.DID]struct{}, len(dlgs))
64+
for _, d := range dlgs {
65+
if _, dup := byIssuer[d.Issuer()]; dup {
66+
return nil, fmt.Errorf("proof chain has two delegations with the same issuer %s (branched chain)", d.Issuer())
67+
}
68+
byIssuer[d.Issuer()] = d
69+
audiences[d.Audience()] = struct{}{}
70+
}
71+
72+
var root ucan.Delegation
73+
for _, d := range dlgs {
74+
if _, isAudience := audiences[d.Issuer()]; isAudience {
75+
continue
76+
}
77+
if root != nil {
78+
return nil, fmt.Errorf("proof chain has multiple roots (issuers %s and %s have no incoming edge)", root.Issuer(), d.Issuer())
79+
}
80+
root = d
81+
}
82+
if root == nil {
83+
return nil, fmt.Errorf("proof chain has no root (cycle)")
84+
}
85+
86+
ordered := make([]ucan.Delegation, 0, len(dlgs))
87+
cur := root
88+
for cur != nil {
89+
ordered = append(ordered, cur)
90+
next, ok := byIssuer[cur.Audience()]
91+
if !ok {
92+
break
93+
}
94+
cur = next
95+
}
96+
97+
if len(ordered) != len(dlgs) {
98+
return nil, fmt.Errorf("proof chain is disconnected: %d delegations supplied but only %d form a contiguous chain", len(dlgs), len(ordered))
99+
}
100+
return ordered, nil
29101
}
30102

31103
type ServicesConfig struct {
@@ -102,13 +174,16 @@ func (s *IndexingServiceConfig) ToAppConfig() (app.IndexingServiceConfig, error)
102174
DID: sdid,
103175
Client: c,
104176
}
105-
// Parse indexing service proofs if provided
177+
// Parse indexing service proof chain if provided
106178
if s.Proof != "" {
107-
dlg, err := delegation.Decode(decodeProof(s.Proof))
179+
chain, err := decodeProofChain(s.Proof)
108180
if err != nil {
109181
return app.IndexingServiceConfig{}, fmt.Errorf("parsing indexing service proof: %w", err)
110182
}
111-
out.Proofs = dlg
183+
if len(chain) == 0 {
184+
return app.IndexingServiceConfig{}, fmt.Errorf("indexing service proof container is empty")
185+
}
186+
out.Proofs = chain
112187
} else {
113188
// TODO(forrest): in the event a node is run without an indexing service proof, it will
114189
// almost always fail to index...obviously.
@@ -174,13 +249,16 @@ func (c *EgressTrackerServiceConfig) ToAppConfig() (app.EgressTrackerServiceConf
174249
CleanupCheckInterval: 1 * time.Hour,
175250
}
176251

177-
// Parse egress tracker service proofs if provided
252+
// Parse egress tracker service proof chain if provided
178253
if c.Proof != "" {
179-
dlg, err := delegation.Decode(decodeProof(c.Proof))
254+
chain, err := decodeProofChain(c.Proof)
180255
if err != nil {
181256
return app.EgressTrackerServiceConfig{}, fmt.Errorf("parsing egress tracker service proof: %w", err)
182257
}
183-
out.Proofs = dlg
258+
if len(chain) == 0 {
259+
return app.EgressTrackerServiceConfig{}, fmt.Errorf("egress tracker service proof container is empty")
260+
}
261+
out.Proofs = chain
184262
} else {
185263
log.Warn("no egress tracker service proof provided, egress tracking is disabled")
186264
}

pkg/service/egresstracker/service.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"github.com/fil-forge/ucantone/did"
2121
"github.com/fil-forge/ucantone/principal"
2222
"github.com/fil-forge/ucantone/ucan"
23-
"github.com/fil-forge/ucantone/ucan/delegation"
2423
"github.com/fil-forge/ucantone/ucan/invocation"
2524
"github.com/ipfs/go-cid"
2625
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
@@ -40,7 +39,11 @@ const journalRotationPeriod = time.Hour * 12
4039
type Service struct {
4140
id principal.Signer
4241
egressTrackerDID did.DID
43-
egressTrackerProofs ucan.Delegation
42+
// egressTrackerProofs is the ordered chain (root → leaf) issued by the
43+
// delegator for the egress-tracker service. Every link must accompany
44+
// each /space/egress/track invocation for the egress-tracker validator
45+
// to walk the chain back to the originating authority.
46+
egressTrackerProofs []ucan.Delegation
4447
egressTrackerConn client.Connection
4548
batchEndpoint *url.URL
4649
journal retrievaljournal.Journal
@@ -56,7 +59,7 @@ type Service struct {
5659
func New(
5760
id principal.Signer,
5861
egressTrackerConn client.Connection,
59-
egressTrackerProofs delegation.Delegation,
62+
egressTrackerProofs []ucan.Delegation,
6063
batchEndpoint *url.URL,
6164
journal retrievaljournal.Journal,
6265
consolidationStore consolidationstore.Store,
@@ -123,14 +126,21 @@ func (s *Service) enqueueEgressTrackTask(ctx context.Context, batchCID cid.Cid)
123126
}
124127
125128
func (s *Service) egressTrack(ctx context.Context, batchCID cid.Cid) error {
129+
if len(s.egressTrackerProofs) == 0 {
130+
return fmt.Errorf("no proofs configured for egress tracker invocation")
131+
}
132+
proofLinks := make([]cid.Cid, len(s.egressTrackerProofs))
133+
for i, d := range s.egressTrackerProofs {
134+
proofLinks[i] = d.Link()
135+
}
126136
trackInv, err := egress.Track.Invoke(
127137
s.id,
128138
s.egressTrackerDID,
129139
&egress.TrackArguments{
130140
Receipts: batchCID,
131141
Endpoint: capabilities.CborURL(*s.batchEndpoint),
132142
},
133-
invocation.WithProofs(s.egressTrackerProofs.Link()),
143+
invocation.WithProofs(proofLinks...),
134144
invocation.WithNoExpiration(),
135145
)
136146
if err != nil {

pkg/service/publisher/options.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type options struct {
1616
announceAddr multiaddr.Multiaddr
1717
announceURLs []url.URL
1818
indexingService app.IndexingServiceConfig
19-
indexingServiceProofs ucan.Delegation
19+
indexingServiceProofs []ucan.Delegation
2020
}
2121

2222
type Option func(*options) error
@@ -54,11 +54,14 @@ func WithIndexingService(conn app.IndexingServiceConfig) Option {
5454
}
5555
}
5656

57-
// WithIndexingServiceProof configures proofs for UCAN invocations to the
58-
// indexing service.
59-
func WithIndexingServiceProof(proof ucan.Delegation) Option {
57+
// WithIndexingServiceProof configures the proof chain (root → leaf) for UCAN
58+
// invocations to the indexing service. Every delegation in the chain is
59+
// attached to each /claim/cache invocation; passing only the leaf yields
60+
// "delegation issuer is X not Y" failures inside the indexer's validator
61+
// because the chain back to the original authority is broken.
62+
func WithIndexingServiceProof(proofs []ucan.Delegation) Option {
6063
return func(opts *options) error {
61-
opts.indexingServiceProofs = proof
64+
opts.indexingServiceProofs = proofs
6265
return nil
6366
}
6467
}

0 commit comments

Comments
 (0)