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  }