git.gammaspectra.live/P2Pool/consensus@v0.0.0-20240403173234-a039820b20c9/monero/client/zmq/zmq.go (about)

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