github.com/celestiaorg/celestia-node@v0.15.0-beta.1/share/p2p/shrexnd/client.go (about)

     1  package shrexnd
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"time"
    10  
    11  	"github.com/libp2p/go-libp2p/core/host"
    12  	"github.com/libp2p/go-libp2p/core/network"
    13  	"github.com/libp2p/go-libp2p/core/peer"
    14  	"github.com/libp2p/go-libp2p/core/protocol"
    15  
    16  	"github.com/celestiaorg/go-libp2p-messenger/serde"
    17  	"github.com/celestiaorg/nmt"
    18  
    19  	"github.com/celestiaorg/celestia-node/share"
    20  	"github.com/celestiaorg/celestia-node/share/p2p"
    21  	pb "github.com/celestiaorg/celestia-node/share/p2p/shrexnd/pb"
    22  )
    23  
    24  // Client implements client side of shrex/nd protocol to obtain namespaced shares data from remote
    25  // peers.
    26  type Client struct {
    27  	params     *Parameters
    28  	protocolID protocol.ID
    29  
    30  	host    host.Host
    31  	metrics *p2p.Metrics
    32  }
    33  
    34  // NewClient creates a new shrEx/nd client
    35  func NewClient(params *Parameters, host host.Host) (*Client, error) {
    36  	if err := params.Validate(); err != nil {
    37  		return nil, fmt.Errorf("shrex-nd: client creation failed: %w", err)
    38  	}
    39  
    40  	return &Client{
    41  		host:       host,
    42  		protocolID: p2p.ProtocolID(params.NetworkID(), protocolString),
    43  		params:     params,
    44  	}, nil
    45  }
    46  
    47  // RequestND requests namespaced data from the given peer.
    48  // Returns NamespacedShares with unverified inclusion proofs against the share.Root.
    49  func (c *Client) RequestND(
    50  	ctx context.Context,
    51  	root *share.Root,
    52  	namespace share.Namespace,
    53  	peer peer.ID,
    54  ) (share.NamespacedShares, error) {
    55  	if err := namespace.ValidateForData(); err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	shares, err := c.doRequest(ctx, root, namespace, peer)
    60  	if err == nil {
    61  		return shares, nil
    62  	}
    63  	if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
    64  		c.metrics.ObserveRequests(ctx, 1, p2p.StatusTimeout)
    65  		return nil, err
    66  	}
    67  	// some net.Errors also mean the context deadline was exceeded, but yamux/mocknet do not
    68  	// unwrap to a ctx err
    69  	var ne net.Error
    70  	if errors.As(err, &ne) && ne.Timeout() {
    71  		if deadline, _ := ctx.Deadline(); deadline.Before(time.Now()) {
    72  			c.metrics.ObserveRequests(ctx, 1, p2p.StatusTimeout)
    73  			return nil, context.DeadlineExceeded
    74  		}
    75  	}
    76  	if err != p2p.ErrNotFound && err != p2p.ErrRateLimited {
    77  		log.Warnw("client-nd: peer returned err", "err", err)
    78  	}
    79  	return nil, err
    80  }
    81  
    82  func (c *Client) doRequest(
    83  	ctx context.Context,
    84  	root *share.Root,
    85  	namespace share.Namespace,
    86  	peerID peer.ID,
    87  ) (share.NamespacedShares, error) {
    88  	stream, err := c.host.NewStream(ctx, peerID, c.protocolID)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	defer stream.Close()
    93  
    94  	c.setStreamDeadlines(ctx, stream)
    95  
    96  	req := &pb.GetSharesByNamespaceRequest{
    97  		RootHash:  root.Hash(),
    98  		Namespace: namespace,
    99  	}
   100  
   101  	_, err = serde.Write(stream, req)
   102  	if err != nil {
   103  		c.metrics.ObserveRequests(ctx, 1, p2p.StatusSendReqErr)
   104  		stream.Reset() //nolint:errcheck
   105  		return nil, fmt.Errorf("client-nd: writing request: %w", err)
   106  	}
   107  
   108  	err = stream.CloseWrite()
   109  	if err != nil {
   110  		log.Debugw("client-nd: closing write side of the stream", "err", err)
   111  	}
   112  
   113  	if err := c.readStatus(ctx, stream); err != nil {
   114  		return nil, err
   115  	}
   116  	return c.readNamespacedShares(ctx, stream)
   117  }
   118  
   119  func (c *Client) readStatus(ctx context.Context, stream network.Stream) error {
   120  	var resp pb.GetSharesByNamespaceStatusResponse
   121  	_, err := serde.Read(stream, &resp)
   122  	if err != nil {
   123  		// server is overloaded and closed the stream
   124  		if errors.Is(err, io.EOF) {
   125  			c.metrics.ObserveRequests(ctx, 1, p2p.StatusRateLimited)
   126  			return p2p.ErrRateLimited
   127  		}
   128  		c.metrics.ObserveRequests(ctx, 1, p2p.StatusReadRespErr)
   129  		stream.Reset() //nolint:errcheck
   130  		return fmt.Errorf("client-nd: reading status response: %w", err)
   131  	}
   132  
   133  	return c.convertStatusToErr(ctx, resp.Status)
   134  }
   135  
   136  // readNamespacedShares converts proto Rows to share.NamespacedShares
   137  func (c *Client) readNamespacedShares(
   138  	ctx context.Context,
   139  	stream network.Stream,
   140  ) (share.NamespacedShares, error) {
   141  	var shares share.NamespacedShares
   142  	for {
   143  		var row pb.NamespaceRowResponse
   144  		_, err := serde.Read(stream, &row)
   145  		if err != nil {
   146  			if errors.Is(err, io.EOF) {
   147  				// all data is received and steam is closed by server
   148  				return shares, nil
   149  			}
   150  			c.metrics.ObserveRequests(ctx, 1, p2p.StatusReadRespErr)
   151  			return nil, err
   152  		}
   153  		var proof nmt.Proof
   154  		if row.Proof != nil {
   155  			if len(row.Shares) != 0 {
   156  				proof = nmt.NewInclusionProof(
   157  					int(row.Proof.Start),
   158  					int(row.Proof.End),
   159  					row.Proof.Nodes,
   160  					row.Proof.IsMaxNamespaceIgnored,
   161  				)
   162  			} else {
   163  				proof = nmt.NewAbsenceProof(
   164  					int(row.Proof.Start),
   165  					int(row.Proof.End),
   166  					row.Proof.Nodes,
   167  					row.Proof.LeafHash,
   168  					row.Proof.IsMaxNamespaceIgnored,
   169  				)
   170  			}
   171  		}
   172  		shares = append(shares, share.NamespacedRow{
   173  			Shares: row.Shares,
   174  			Proof:  &proof,
   175  		})
   176  	}
   177  }
   178  
   179  func (c *Client) setStreamDeadlines(ctx context.Context, stream network.Stream) {
   180  	// set read/write deadline to use context deadline if it exists
   181  	deadline, ok := ctx.Deadline()
   182  	if ok {
   183  		err := stream.SetDeadline(deadline)
   184  		if err == nil {
   185  			return
   186  		}
   187  		log.Debugw("client-nd: set stream deadline", "err", err)
   188  	}
   189  
   190  	// if deadline not set, client read deadline defaults to server write deadline
   191  	if c.params.ServerWriteTimeout != 0 {
   192  		err := stream.SetReadDeadline(time.Now().Add(c.params.ServerWriteTimeout))
   193  		if err != nil {
   194  			log.Debugw("client-nd: set read deadline", "err", err)
   195  		}
   196  	}
   197  
   198  	// if deadline not set, client write deadline defaults to server read deadline
   199  	if c.params.ServerReadTimeout != 0 {
   200  		err := stream.SetWriteDeadline(time.Now().Add(c.params.ServerReadTimeout))
   201  		if err != nil {
   202  			log.Debugw("client-nd: set write deadline", "err", err)
   203  		}
   204  	}
   205  }
   206  
   207  func (c *Client) convertStatusToErr(ctx context.Context, status pb.StatusCode) error {
   208  	switch status {
   209  	case pb.StatusCode_OK:
   210  		c.metrics.ObserveRequests(ctx, 1, p2p.StatusSuccess)
   211  		return nil
   212  	case pb.StatusCode_NOT_FOUND:
   213  		c.metrics.ObserveRequests(ctx, 1, p2p.StatusNotFound)
   214  		return p2p.ErrNotFound
   215  	case pb.StatusCode_INVALID:
   216  		log.Warn("client-nd: invalid request")
   217  		fallthrough
   218  	case pb.StatusCode_INTERNAL:
   219  		fallthrough
   220  	default:
   221  		return p2p.ErrInvalidResponse
   222  	}
   223  }