github.com/sunrise-zone/sunrise-node@v0.13.1-sr2/share/ipld/namespace_data.go (about) 1 package ipld 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "sync/atomic" 9 10 "github.com/ipfs/boxo/blockservice" 11 "github.com/ipfs/go-cid" 12 ipld "github.com/ipfs/go-ipld-format" 13 14 "github.com/celestiaorg/nmt" 15 16 "github.com/sunrise-zone/sunrise-node/share" 17 ) 18 19 var ErrNamespaceOutsideRange = errors.New("share/ipld: " + 20 "target namespace is outside of namespace range for the given root") 21 22 // Option is the functional option that is applied to the NamespaceData instance 23 // to configure data that needs to be stored. 24 type Option func(*NamespaceData) 25 26 // WithLeaves option specifies that leaves should be collected during retrieval. 27 func WithLeaves() Option { 28 return func(data *NamespaceData) { 29 // we over-allocate space for leaves since we do not know how many we will find 30 // on the level above, the length of the Row is passed in as maxShares 31 data.leaves = make([]ipld.Node, data.maxShares) 32 } 33 } 34 35 // WithProofs option specifies that proofs should be collected during retrieval. 36 func WithProofs() Option { 37 return func(data *NamespaceData) { 38 data.proofs = newProofCollector(data.maxShares) 39 } 40 } 41 42 // NamespaceData stores all leaves under the given namespace with their corresponding proofs. 43 type NamespaceData struct { 44 leaves []ipld.Node 45 proofs *proofCollector 46 47 bounds fetchedBounds 48 maxShares int 49 namespace share.Namespace 50 51 isAbsentNamespace atomic.Bool 52 absenceProofLeaf ipld.Node 53 } 54 55 func NewNamespaceData(maxShares int, namespace share.Namespace, options ...Option) *NamespaceData { 56 data := &NamespaceData{ 57 // we don't know where in the tree the leaves in the namespace are, 58 // so we keep track of the bounds to return the correct slice 59 // maxShares acts as a sentinel to know if we find any leaves 60 bounds: fetchedBounds{int64(maxShares), 0}, 61 maxShares: maxShares, 62 namespace: namespace, 63 } 64 65 for _, opt := range options { 66 opt(data) 67 } 68 return data 69 } 70 71 func (n *NamespaceData) validate(rootCid cid.Cid) error { 72 if err := n.namespace.Validate(); err != nil { 73 return err 74 } 75 76 if n.leaves == nil && n.proofs == nil { 77 return errors.New("share/ipld: empty NamespaceData, nothing specified to retrieve") 78 } 79 80 root := NamespacedSha256FromCID(rootCid) 81 if n.namespace.IsOutsideRange(root, root) { 82 return ErrNamespaceOutsideRange 83 } 84 return nil 85 } 86 87 func (n *NamespaceData) addLeaf(pos int, nd ipld.Node) { 88 // bounds will be needed in `Proof` method 89 n.bounds.update(int64(pos)) 90 91 if n.isAbsentNamespace.Load() { 92 if n.absenceProofLeaf != nil { 93 log.Fatal("there should be only one absence leaf") 94 } 95 n.absenceProofLeaf = nd 96 return 97 } 98 99 if n.leaves == nil { 100 return 101 } 102 103 if nd != nil { 104 n.leaves[pos] = nd 105 } 106 } 107 108 // noLeaves checks that there are no leaves under the given root in the given namespace. 109 func (n *NamespaceData) noLeaves() bool { 110 return n.bounds.lowest == int64(n.maxShares) 111 } 112 113 type direction int 114 115 const ( 116 left direction = iota + 1 117 right 118 ) 119 120 func (n *NamespaceData) addProof(d direction, cid cid.Cid, depth int) { 121 if n.proofs == nil { 122 return 123 } 124 125 switch d { 126 case left: 127 n.proofs.addLeft(cid, depth) 128 case right: 129 n.proofs.addRight(cid, depth) 130 default: 131 panic(fmt.Sprintf("share/ipld: invalid direction: %d", d)) 132 } 133 } 134 135 // Leaves returns retrieved leaves within the bounds in case `WithLeaves` option was passed, 136 // otherwise nil will be returned. 137 func (n *NamespaceData) Leaves() []ipld.Node { 138 if n.leaves == nil || n.noLeaves() || n.isAbsentNamespace.Load() { 139 return nil 140 } 141 return n.leaves[n.bounds.lowest : n.bounds.highest+1] 142 } 143 144 // Proof returns proofs within the bounds in case if `WithProofs` option was passed, 145 // otherwise nil will be returned. 146 func (n *NamespaceData) Proof() *nmt.Proof { 147 if n.proofs == nil { 148 return nil 149 } 150 151 // return an empty Proof if leaves are not available 152 if n.noLeaves() { 153 return &nmt.Proof{} 154 } 155 156 nodes := make([][]byte, len(n.proofs.Nodes())) 157 for i, node := range n.proofs.Nodes() { 158 nodes[i] = NamespacedSha256FromCID(node) 159 } 160 161 if n.isAbsentNamespace.Load() { 162 proof := nmt.NewAbsenceProof( 163 int(n.bounds.lowest), 164 int(n.bounds.highest)+1, 165 nodes, 166 NamespacedSha256FromCID(n.absenceProofLeaf.Cid()), 167 NMTIgnoreMaxNamespace, 168 ) 169 return &proof 170 } 171 proof := nmt.NewInclusionProof( 172 int(n.bounds.lowest), 173 int(n.bounds.highest)+1, 174 nodes, 175 NMTIgnoreMaxNamespace, 176 ) 177 return &proof 178 } 179 180 // CollectLeavesByNamespace collects leaves and corresponding proof that could be used to verify 181 // leaves inclusion. It returns as many leaves from the given root with the given Namespace as 182 // it can retrieve. If no shares are found, it returns error as nil. A 183 // non-nil error means that only partial data is returned, because at least one share retrieval 184 // failed. The following implementation is based on `GetShares`. 185 func (n *NamespaceData) CollectLeavesByNamespace( 186 ctx context.Context, 187 bGetter blockservice.BlockGetter, 188 root cid.Cid, 189 ) error { 190 if err := n.validate(root); err != nil { 191 return err 192 } 193 194 // buffer the jobs to avoid blocking, we only need as many 195 // queued as the number of shares in the second-to-last layer 196 jobs := make(chan job, (n.maxShares+1)/2) 197 jobs <- job{cid: root, ctx: ctx} 198 199 var wg chanGroup 200 wg.jobs = jobs 201 wg.add(1) 202 203 var ( 204 singleErr sync.Once 205 retrievalErr error 206 ) 207 208 for { 209 var j job 210 var ok bool 211 select { 212 case j, ok = <-jobs: 213 case <-ctx.Done(): 214 return ctx.Err() 215 } 216 217 if !ok { 218 return retrievalErr 219 } 220 pool.Submit(func() { 221 defer wg.done() 222 223 // if an error is likely to be returned or not depends on 224 // the underlying impl of the blockservice, currently it is not a realistic probability 225 nd, err := GetNode(ctx, bGetter, j.cid) 226 if err != nil { 227 singleErr.Do(func() { 228 retrievalErr = err 229 }) 230 log.Errorw("could not retrieve IPLD node", 231 "namespace", n.namespace.String(), 232 "pos", j.sharePos, 233 "err", err, 234 ) 235 // we still need to update the bounds 236 n.addLeaf(j.sharePos, nil) 237 return 238 } 239 240 links := nd.Links() 241 if len(links) == 0 { 242 // successfully fetched a leaf belonging to the namespace 243 // we found a leaf, so we update the bounds 244 n.addLeaf(j.sharePos, nd) 245 return 246 } 247 248 // this node has links in the namespace, so keep walking 249 newJobs := n.traverseLinks(j, links) 250 for _, j := range newJobs { 251 wg.add(1) 252 select { 253 case jobs <- j: 254 case <-ctx.Done(): 255 return 256 } 257 } 258 }) 259 } 260 } 261 262 func (n *NamespaceData) traverseLinks(j job, links []*ipld.Link) []job { 263 if j.isAbsent { 264 return n.collectAbsenceProofs(j, links) 265 } 266 return n.collectNDWithProofs(j, links) 267 } 268 269 func (n *NamespaceData) collectAbsenceProofs(j job, links []*ipld.Link) []job { 270 leftLink := links[0].Cid 271 rightLink := links[1].Cid 272 // traverse to the left node, while collecting right node as proof 273 n.addProof(right, rightLink, j.depth) 274 return []job{j.next(left, leftLink, j.isAbsent)} 275 } 276 277 func (n *NamespaceData) collectNDWithProofs(j job, links []*ipld.Link) []job { 278 leftCid := links[0].Cid 279 rightCid := links[1].Cid 280 leftLink := NamespacedSha256FromCID(leftCid) 281 rightLink := NamespacedSha256FromCID(rightCid) 282 283 var nextJobs []job 284 // check if target namespace is outside of boundaries of both links 285 if n.namespace.IsOutsideRange(leftLink, rightLink) { 286 log.Fatalf("target namespace outside of boundaries of links at depth: %v", j.depth) 287 } 288 289 if !n.namespace.IsAboveMax(leftLink) { 290 // namespace is within the range of left link 291 nextJobs = append(nextJobs, j.next(left, leftCid, false)) 292 } else { 293 // proof is on the left side, if the namespace is on the right side of the range of left link 294 n.addProof(left, leftCid, j.depth) 295 if n.namespace.IsBelowMin(rightLink) { 296 // namespace is not included in either links, convert to absence collector 297 n.isAbsentNamespace.Store(true) 298 nextJobs = append(nextJobs, j.next(right, rightCid, true)) 299 return nextJobs 300 } 301 } 302 303 if !n.namespace.IsBelowMin(rightLink) { 304 // namespace is within the range of right link 305 nextJobs = append(nextJobs, j.next(right, rightCid, false)) 306 } else { 307 // proof is on the right side, if the namespace is on the left side of the range of right link 308 n.addProof(right, rightCid, j.depth) 309 } 310 return nextJobs 311 } 312 313 type fetchedBounds struct { 314 lowest int64 315 highest int64 316 } 317 318 // update checks if the passed index is outside the current bounds, 319 // and updates the bounds atomically if it extends them. 320 func (b *fetchedBounds) update(index int64) { 321 lowest := atomic.LoadInt64(&b.lowest) 322 // try to write index to the lower bound if appropriate, and retry until the atomic op is successful 323 // CAS ensures that we don't overwrite if the bound has been updated in another goroutine after the 324 // comparison here 325 for index < lowest && !atomic.CompareAndSwapInt64(&b.lowest, lowest, index) { 326 lowest = atomic.LoadInt64(&b.lowest) 327 } 328 // we always run both checks because element can be both the lower and higher bound 329 // for example, if there is only one share in the namespace 330 highest := atomic.LoadInt64(&b.highest) 331 for index > highest && !atomic.CompareAndSwapInt64(&b.highest, highest, index) { 332 highest = atomic.LoadInt64(&b.highest) 333 } 334 }