@@ -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
31103type 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 }
0 commit comments