git.gammaspectra.live/P2Pool/consensus/v3@v3.8.0/monero/client/zmq/zmq.go (about)

     1  package zmq
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"git.gammaspectra.live/P2Pool/consensus/v3/p2pool/mempool"
     8  	"git.gammaspectra.live/P2Pool/consensus/v3/utils"
     9  	"slices"
    10  	"strings"
    11  
    12  	"git.gammaspectra.live/P2Pool/zmq4"
    13  )
    14  
    15  type Client struct {
    16  	endpoint string
    17  	topics   []Topic
    18  	sub      zmq4.Socket
    19  }
    20  
    21  // NewClient instantiates a new client that will receive monerod's zmq events.
    22  //
    23  //   - `topics` is a list of fully-formed zmq topic to subscribe to
    24  //
    25  //   - `endpoint` is the full address where monerod has been configured to
    26  //     publish the messages to, including the network schama. for instance,
    27  //     considering that monerod has been started with
    28  //
    29  //     monerod --zmq-pub tcp://127.0.0.1:18085
    30  //
    31  //     `endpoint` should be 'tcp://127.0.0.1:18085'.
    32  func NewClient(endpoint string, topics ...Topic) *Client {
    33  	return &Client{
    34  		endpoint: endpoint,
    35  		topics:   topics,
    36  	}
    37  }
    38  
    39  // Stream provides channels where instances of the desired topic object are
    40  // sent to.
    41  type Stream struct {
    42  	FullChainMainC    func(*FullChainMain)
    43  	FullTxPoolAddC    func([]FullTxPoolAdd)
    44  	FullMinerDataC    func(*FullMinerData)
    45  	MinimalChainMainC func(*MinimalChainMain)
    46  	MinimalTxPoolAddC func(mempool.Mempool)
    47  }
    48  
    49  // Listen listens for a list of topics pre-configured for this client (via NewClient).
    50  func (c *Client) Listen(ctx context.Context, fullChainMain func(chainMain *FullChainMain), fullTxPoolAdd func(txs []FullTxPoolAdd), fullMinerData func(main *FullMinerData), minimalChainMain func(chainMain *MinimalChainMain), minimalTxPoolAdd func(txs mempool.Mempool)) error {
    51  	if err := c.listen(ctx, c.topics...); err != nil {
    52  		return fmt.Errorf("listen on '%s': %w", strings.Join(func() (r []string) {
    53  			for _, s := range c.topics {
    54  				r = append(r, string(s))
    55  			}
    56  			return r
    57  		}(), ", "), err)
    58  	}
    59  
    60  	stream := &Stream{
    61  		FullChainMainC:    fullChainMain,
    62  		FullTxPoolAddC:    fullTxPoolAdd,
    63  		FullMinerDataC:    fullMinerData,
    64  		MinimalChainMainC: minimalChainMain,
    65  		MinimalTxPoolAddC: minimalTxPoolAdd,
    66  	}
    67  
    68  	if err := c.loop(stream); err != nil {
    69  		return fmt.Errorf("loop: %w", err)
    70  	}
    71  
    72  	return nil
    73  }
    74  
    75  // Close closes any established connection, if any.
    76  func (c *Client) Close() error {
    77  	if c.sub == nil {
    78  		return nil
    79  	}
    80  
    81  	return c.sub.Close()
    82  }
    83  
    84  func (c *Client) listen(ctx context.Context, topics ...Topic) error {
    85  	c.sub = zmq4.NewSub(ctx)
    86  
    87  	err := c.sub.Dial(c.endpoint)
    88  	if err != nil {
    89  		return fmt.Errorf("dial '%s': %w", c.endpoint, err)
    90  	}
    91  
    92  	for _, topic := range topics {
    93  		err = c.sub.SetOption(zmq4.OptionSubscribe, string(topic))
    94  		if err != nil {
    95  			return fmt.Errorf("subscribe: %w", err)
    96  		}
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  func (c *Client) loop(stream *Stream) error {
   103  	for {
   104  		msg, err := c.sub.Recv()
   105  		if err != nil {
   106  			return fmt.Errorf("recv: %w", err)
   107  		}
   108  
   109  		for _, frame := range msg.Frames {
   110  			err := c.ingestFrameArray(stream, frame)
   111  			if err != nil {
   112  				return fmt.Errorf("consume frame: %w", err)
   113  			}
   114  		}
   115  	}
   116  }
   117  
   118  func (c *Client) ingestFrameArray(stream *Stream, frame []byte) error {
   119  	topic, gson, err := jsonFromFrame(frame)
   120  	if err != nil {
   121  		return fmt.Errorf("json from frame: %w", err)
   122  	}
   123  
   124  	if slices.Index(c.topics, topic) == -1 {
   125  		return fmt.Errorf("topic '%s' doesn't match "+
   126  			"expected any of '%s'", topic, strings.Join(func() (r []string) {
   127  			for _, s := range c.topics {
   128  				r = append(r, string(s))
   129  			}
   130  			return r
   131  		}(), ", "))
   132  	}
   133  
   134  	switch Topic(topic) {
   135  	case TopicFullChainMain:
   136  		return c.transmitFullChainMain(stream, gson)
   137  	case TopicFullTxPoolAdd:
   138  		return c.transmitFullTxPoolAdd(stream, gson)
   139  	case TopicFullMinerData:
   140  		return c.transmitFullMinerData(stream, gson)
   141  	case TopicMinimalChainMain:
   142  		return c.transmitMinimalChainMain(stream, gson)
   143  	case TopicMinimalTxPoolAdd:
   144  		return c.transmitMinimalTxPoolAdd(stream, gson)
   145  	default:
   146  		return fmt.Errorf("unhandled topic '%s'", topic)
   147  	}
   148  }
   149  
   150  func (c *Client) transmitFullChainMain(stream *Stream, gson []byte) error {
   151  	var arr []*FullChainMain
   152  
   153  	if err := utils.UnmarshalJSON(gson, &arr); err != nil {
   154  		return fmt.Errorf("unmarshal: %w", err)
   155  	}
   156  	for _, element := range arr {
   157  		stream.FullChainMainC(element)
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  func (c *Client) transmitFullTxPoolAdd(stream *Stream, gson []byte) error {
   164  	var arr []FullTxPoolAdd
   165  
   166  	if err := utils.UnmarshalJSON(gson, &arr); err != nil {
   167  		return fmt.Errorf("unmarshal: %w", err)
   168  	}
   169  	stream.FullTxPoolAddC(arr)
   170  
   171  	return nil
   172  }
   173  
   174  func (c *Client) transmitFullMinerData(stream *Stream, gson []byte) error {
   175  	element := &FullMinerData{}
   176  
   177  	if err := utils.UnmarshalJSON(gson, element); err != nil {
   178  		return fmt.Errorf("unmarshal: %w", err)
   179  	}
   180  	stream.FullMinerDataC(element)
   181  	return nil
   182  }
   183  
   184  func (c *Client) transmitMinimalChainMain(stream *Stream, gson []byte) error {
   185  	element := &MinimalChainMain{}
   186  
   187  	if err := utils.UnmarshalJSON(gson, element); err != nil {
   188  		return fmt.Errorf("unmarshal: %w", err)
   189  	}
   190  	stream.MinimalChainMainC(element)
   191  	return nil
   192  }
   193  
   194  func (c *Client) transmitMinimalTxPoolAdd(stream *Stream, gson []byte) error {
   195  	var arr mempool.Mempool
   196  
   197  	if err := utils.UnmarshalJSON(gson, &arr); err != nil {
   198  		return fmt.Errorf("unmarshal: %w", err)
   199  	}
   200  	stream.MinimalTxPoolAddC(arr)
   201  
   202  	return nil
   203  }
   204  
   205  func jsonFromFrame(frame []byte) (Topic, []byte, error) {
   206  	unknown := TopicUnknown
   207  
   208  	parts := bytes.SplitN(frame, []byte(":"), 2)
   209  	if len(parts) != 2 {
   210  		return unknown, nil, fmt.Errorf(
   211  			"malformed: expected 2 parts, got %d", len(parts))
   212  	}
   213  
   214  	topic, gson := string(parts[0]), parts[1]
   215  
   216  	switch topic {
   217  	case string(TopicMinimalChainMain):
   218  		return TopicMinimalChainMain, gson, nil
   219  	case string(TopicFullChainMain):
   220  		return TopicFullChainMain, gson, nil
   221  	case string(TopicFullMinerData):
   222  		return TopicFullMinerData, gson, nil
   223  	case string(TopicFullTxPoolAdd):
   224  		return TopicFullTxPoolAdd, gson, nil
   225  	case string(TopicMinimalTxPoolAdd):
   226  		return TopicMinimalTxPoolAdd, gson, nil
   227  	}
   228  
   229  	return unknown, nil, fmt.Errorf("unknown topic '%s'", topic)
   230  }