github.com/badrootd/celestia-core@v0.0.0-20240305091328-aa4207a4b25d/mempool/cat/reactor.go (about)

     1  package cat
     2  
     3  import (
     4  	"fmt"
     5  	"math/rand"
     6  	"time"
     7  
     8  	"github.com/gogo/protobuf/proto"
     9  
    10  	cfg "github.com/badrootd/celestia-core/config"
    11  	"github.com/badrootd/celestia-core/crypto/tmhash"
    12  	"github.com/badrootd/celestia-core/libs/log"
    13  	"github.com/badrootd/celestia-core/mempool"
    14  	"github.com/badrootd/celestia-core/p2p"
    15  	"github.com/badrootd/celestia-core/pkg/trace"
    16  	"github.com/badrootd/celestia-core/pkg/trace/schema"
    17  	protomem "github.com/badrootd/celestia-core/proto/tendermint/mempool"
    18  	"github.com/badrootd/celestia-core/types"
    19  )
    20  
    21  const (
    22  	// default duration to wait before considering a peer non-responsive
    23  	// and searching for the tx from a new peer
    24  	DefaultGossipDelay = 200 * time.Millisecond
    25  
    26  	// Content Addressable Tx Pool gossips state based messages (SeenTx and WantTx) on a separate channel
    27  	// for cross compatibility
    28  	MempoolStateChannel = byte(0x31)
    29  
    30  	// peerHeightDiff signifies the tolerance in difference in height between the peer and the height
    31  	// the node received the tx
    32  	peerHeightDiff = 10
    33  )
    34  
    35  // Reactor handles mempool tx broadcasting logic amongst peers. For the main
    36  // logic behind the protocol, refer to `ReceiveEnvelope` or to the english
    37  // spec under /.spec.md
    38  type Reactor struct {
    39  	p2p.BaseReactor
    40  	opts        *ReactorOptions
    41  	mempool     *TxPool
    42  	ids         *mempoolIDs
    43  	requests    *requestScheduler
    44  	traceClient *trace.Client
    45  }
    46  
    47  type ReactorOptions struct {
    48  	// ListenOnly means that the node will never broadcast any of the transactions that
    49  	// it receives. This is useful for keeping transactions private
    50  	ListenOnly bool
    51  
    52  	// MaxTxSize is the maximum size of a transaction that can be received
    53  	MaxTxSize int
    54  
    55  	// MaxGossipDelay is the maximum allotted time that the reactor expects a transaction to
    56  	// arrive before issuing a new request to a different peer
    57  	MaxGossipDelay time.Duration
    58  
    59  	// TraceClient is the trace client for collecting trace level events
    60  	TraceClient *trace.Client
    61  }
    62  
    63  func (opts *ReactorOptions) VerifyAndComplete() error {
    64  	if opts.MaxTxSize == 0 {
    65  		opts.MaxTxSize = cfg.DefaultMempoolConfig().MaxTxBytes
    66  	}
    67  
    68  	if opts.MaxGossipDelay == 0 {
    69  		opts.MaxGossipDelay = DefaultGossipDelay
    70  	}
    71  
    72  	if opts.MaxTxSize < 0 {
    73  		return fmt.Errorf("max tx size (%d) cannot be negative", opts.MaxTxSize)
    74  	}
    75  
    76  	if opts.MaxGossipDelay < 0 {
    77  		return fmt.Errorf("max gossip delay (%d) cannot be negative", opts.MaxGossipDelay)
    78  	}
    79  
    80  	return nil
    81  }
    82  
    83  // NewReactor returns a new Reactor with the given config and mempool.
    84  func NewReactor(mempool *TxPool, opts *ReactorOptions) (*Reactor, error) {
    85  	err := opts.VerifyAndComplete()
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	memR := &Reactor{
    90  		opts:        opts,
    91  		mempool:     mempool,
    92  		ids:         newMempoolIDs(),
    93  		requests:    newRequestScheduler(opts.MaxGossipDelay, defaultGlobalRequestTimeout),
    94  		traceClient: &trace.Client{},
    95  	}
    96  	memR.BaseReactor = *p2p.NewBaseReactor("Mempool", memR)
    97  	return memR, nil
    98  }
    99  
   100  // SetLogger sets the Logger on the reactor and the underlying mempool.
   101  func (memR *Reactor) SetLogger(l log.Logger) {
   102  	memR.Logger = l
   103  }
   104  
   105  // OnStart implements Service.
   106  func (memR *Reactor) OnStart() error {
   107  	if !memR.opts.ListenOnly {
   108  		go func() {
   109  			for {
   110  				select {
   111  				case <-memR.Quit():
   112  					return
   113  
   114  				// listen in for any newly verified tx via RPC, then immediately
   115  				// broadcast it to all connected peers.
   116  				case nextTx := <-memR.mempool.next():
   117  					memR.broadcastNewTx(nextTx)
   118  				}
   119  			}
   120  		}()
   121  	} else {
   122  		memR.Logger.Info("Tx broadcasting is disabled")
   123  	}
   124  	// run a separate go routine to check for time based TTLs
   125  	if memR.mempool.config.TTLDuration > 0 {
   126  		go func() {
   127  			ticker := time.NewTicker(memR.mempool.config.TTLDuration)
   128  			for {
   129  				select {
   130  				case <-ticker.C:
   131  					memR.mempool.CheckToPurgeExpiredTxs()
   132  				case <-memR.Quit():
   133  					return
   134  				}
   135  			}
   136  		}()
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  // OnStop implements Service
   143  func (memR *Reactor) OnStop() {
   144  	// stop all the timers tracking outbound requests
   145  	memR.requests.Close()
   146  }
   147  
   148  // GetChannels implements Reactor by returning the list of channels for this
   149  // reactor.
   150  func (memR *Reactor) GetChannels() []*p2p.ChannelDescriptor {
   151  	largestTx := make([]byte, memR.opts.MaxTxSize)
   152  	txMsg := protomem.Message{
   153  		Sum: &protomem.Message_Txs{
   154  			Txs: &protomem.Txs{Txs: [][]byte{largestTx}},
   155  		},
   156  	}
   157  
   158  	stateMsg := protomem.Message{
   159  		Sum: &protomem.Message_SeenTx{
   160  			SeenTx: &protomem.SeenTx{
   161  				TxKey: make([]byte, tmhash.Size),
   162  			},
   163  		},
   164  	}
   165  
   166  	return []*p2p.ChannelDescriptor{
   167  		{
   168  			ID:                  mempool.MempoolChannel,
   169  			Priority:            6,
   170  			RecvMessageCapacity: txMsg.Size(),
   171  			MessageType:         &protomem.Message{},
   172  		},
   173  		{
   174  			ID:                  MempoolStateChannel,
   175  			Priority:            5,
   176  			RecvMessageCapacity: stateMsg.Size(),
   177  			MessageType:         &protomem.Message{},
   178  		},
   179  	}
   180  }
   181  
   182  // InitPeer implements Reactor by creating a state for the peer.
   183  func (memR *Reactor) InitPeer(peer p2p.Peer) p2p.Peer {
   184  	memR.ids.ReserveForPeer(peer)
   185  	return peer
   186  }
   187  
   188  // RemovePeer implements Reactor. For all current outbound requests to this
   189  // peer it will find a new peer to rerequest the same transactions.
   190  func (memR *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) {
   191  	peerID := memR.ids.Reclaim(peer.ID())
   192  	// clear all memory of seen txs by that peer
   193  	memR.mempool.seenByPeersSet.RemovePeer(peerID)
   194  
   195  	// remove and rerequest all pending outbound requests to that peer since we know
   196  	// we won't receive any responses from them.
   197  	outboundRequests := memR.requests.ClearAllRequestsFrom(peerID)
   198  	for key := range outboundRequests {
   199  		memR.mempool.metrics.RequestedTxs.Add(1)
   200  		memR.findNewPeerToRequestTx(key)
   201  	}
   202  }
   203  
   204  func (memR *Reactor) Receive(chID byte, peer p2p.Peer, msgBytes []byte) {
   205  	msg := &protomem.Message{}
   206  	err := proto.Unmarshal(msgBytes, msg)
   207  	if err != nil {
   208  		panic(err)
   209  	}
   210  	uw, err := msg.Unwrap()
   211  	if err != nil {
   212  		panic(err)
   213  	}
   214  	memR.ReceiveEnvelope(p2p.Envelope{
   215  		ChannelID: chID,
   216  		Src:       peer,
   217  		Message:   uw,
   218  	})
   219  }
   220  
   221  // ReceiveEnvelope implements Reactor.
   222  // It processes one of three messages: Txs, SeenTx, WantTx.
   223  func (memR *Reactor) ReceiveEnvelope(e p2p.Envelope) {
   224  	switch msg := e.Message.(type) {
   225  
   226  	// A peer has sent us one or more transactions. This could be either because we requested them
   227  	// or because the peer received a new transaction and is broadcasting it to us.
   228  	// NOTE: This setup also means that we can support older mempool implementations that simply
   229  	// flooded the network with transactions.
   230  	case *protomem.Txs:
   231  		for _, tx := range msg.Txs {
   232  			schema.WriteMempoolTx(memR.traceClient, e.Src.ID(), tx, schema.TransferTypeDownload, schema.CatVersionFieldValue)
   233  		}
   234  		protoTxs := msg.GetTxs()
   235  		if len(protoTxs) == 0 {
   236  			memR.Logger.Error("received empty txs from peer", "src", e.Src)
   237  			return
   238  		}
   239  		peerID := memR.ids.GetIDForPeer(e.Src.ID())
   240  		txInfo := mempool.TxInfo{SenderID: peerID}
   241  		txInfo.SenderP2PID = e.Src.ID()
   242  
   243  		var err error
   244  		for _, tx := range protoTxs {
   245  			ntx := types.Tx(tx)
   246  			key := ntx.Key()
   247  			// If we requested the transaction we mark it as received.
   248  			if memR.requests.Has(peerID, key) {
   249  				memR.requests.MarkReceived(peerID, key)
   250  				memR.Logger.Debug("received a response for a requested transaction", "peerID", peerID, "txKey", key)
   251  			} else {
   252  				// If we didn't request the transaction we simply mark the peer as having the
   253  				// tx (we'd have already done it if we were requesting the tx).
   254  				memR.mempool.PeerHasTx(peerID, key)
   255  				memR.Logger.Debug("received new transaction", "peerID", peerID, "txKey", key)
   256  			}
   257  			_, err = memR.mempool.TryAddNewTx(ntx, key, txInfo)
   258  			if err != nil && err != ErrTxInMempool {
   259  				memR.Logger.Debug("Could not add tx", "txKey", key, "err", err)
   260  				return
   261  			}
   262  			if !memR.opts.ListenOnly {
   263  				// We broadcast only transactions that we deem valid and actually have in our mempool.
   264  				memR.broadcastSeenTx(key)
   265  			}
   266  		}
   267  
   268  	// A peer has indicated to us that it has a transaction. We first verify the txkey and
   269  	// mark that peer as having the transaction. Then we proceed with the following logic:
   270  	//
   271  	// 1. If we have the transaction, we do nothing.
   272  	// 2. If we don't yet have the tx but have an outgoing request for it, we do nothing.
   273  	// 3. If we recently evicted the tx and still don't have space for it, we do nothing.
   274  	// 4. Else, we request the transaction from that peer.
   275  	case *protomem.SeenTx:
   276  		schema.WriteMempoolPeerState(
   277  			memR.traceClient,
   278  			e.Src.ID(),
   279  			schema.SeenTxStateUpdateFieldValue,
   280  			schema.TransferTypeDownload,
   281  			schema.CatVersionFieldValue,
   282  		)
   283  		txKey, err := types.TxKeyFromBytes(msg.TxKey)
   284  		if err != nil {
   285  			memR.Logger.Error("peer sent SeenTx with incorrect tx key", "err", err)
   286  			memR.Switch.StopPeerForError(e.Src, err)
   287  			return
   288  		}
   289  		peerID := memR.ids.GetIDForPeer(e.Src.ID())
   290  		memR.mempool.PeerHasTx(peerID, txKey)
   291  		// Check if we don't already have the transaction and that it was recently rejected
   292  		if memR.mempool.Has(txKey) || memR.mempool.IsRejectedTx(txKey) {
   293  			memR.Logger.Debug("received a seen tx for a tx we already have", "txKey", txKey)
   294  			return
   295  		}
   296  
   297  		// If we are already requesting that tx, then we don't need to go any further.
   298  		if memR.requests.ForTx(txKey) != 0 {
   299  			memR.Logger.Debug("received a SeenTx message for a transaction we are already requesting", "txKey", txKey)
   300  			return
   301  		}
   302  
   303  		// We don't have the transaction, nor are we requesting it so we send the node
   304  		// a want msg
   305  		memR.requestTx(txKey, e.Src)
   306  
   307  	// A peer is requesting a transaction that we have claimed to have. Find the specified
   308  	// transaction and broadcast it to the peer. We may no longer have the transaction
   309  	case *protomem.WantTx:
   310  		schema.WriteMempoolPeerState(
   311  			memR.traceClient,
   312  			e.Src.ID(),
   313  			schema.WantTxStateUpdateFieldValue,
   314  			schema.TransferTypeDownload,
   315  			schema.CatVersionFieldValue,
   316  		)
   317  		txKey, err := types.TxKeyFromBytes(msg.TxKey)
   318  		if err != nil {
   319  			memR.Logger.Error("peer sent WantTx with incorrect tx key", "err", err)
   320  			memR.Switch.StopPeerForError(e.Src, err)
   321  			return
   322  		}
   323  		tx, has := memR.mempool.Get(txKey)
   324  		if has && !memR.opts.ListenOnly {
   325  			peerID := memR.ids.GetIDForPeer(e.Src.ID())
   326  			schema.WriteMempoolTx(
   327  				memR.traceClient,
   328  				e.Src.ID(),
   329  				msg.TxKey,
   330  				schema.TransferTypeUpload,
   331  				schema.CatVersionFieldValue,
   332  			)
   333  			memR.Logger.Debug("sending a tx in response to a want msg", "peer", peerID)
   334  			if p2p.SendEnvelopeShim(e.Src, p2p.Envelope{ //nolint:staticcheck
   335  				ChannelID: mempool.MempoolChannel,
   336  				Message:   &protomem.Txs{Txs: [][]byte{tx}},
   337  			}, memR.Logger) {
   338  				memR.mempool.PeerHasTx(peerID, txKey)
   339  			}
   340  		}
   341  
   342  	default:
   343  		memR.Logger.Error("unknown message type", "src", e.Src, "chId", e.ChannelID, "msg", fmt.Sprintf("%T", msg))
   344  		memR.Switch.StopPeerForError(e.Src, fmt.Errorf("mempool cannot handle message of type: %T", msg))
   345  		return
   346  	}
   347  }
   348  
   349  // PeerState describes the state of a peer.
   350  type PeerState interface {
   351  	GetHeight() int64
   352  }
   353  
   354  // broadcastSeenTx broadcasts a SeenTx message to all peers unless we
   355  // know they have already seen the transaction
   356  func (memR *Reactor) broadcastSeenTx(txKey types.TxKey) {
   357  	memR.Logger.Debug("broadcasting seen tx to all peers", "tx_key", txKey.String())
   358  	msg := &protomem.Message{
   359  		Sum: &protomem.Message_SeenTx{
   360  			SeenTx: &protomem.SeenTx{
   361  				TxKey: txKey[:],
   362  			},
   363  		},
   364  	}
   365  
   366  	// Add jitter to when the node broadcasts it's seen txs to stagger when nodes
   367  	// in the network broadcast their seenTx messages.
   368  	time.Sleep(time.Duration(rand.Intn(10)*10) * time.Millisecond) //nolint:gosec
   369  
   370  	for id, peer := range memR.ids.GetAll() {
   371  		if p, ok := peer.Get(types.PeerStateKey).(PeerState); ok {
   372  			// make sure peer isn't too far behind. This can happen
   373  			// if the peer is blocksyncing still and catching up
   374  			// in which case we just skip sending the transaction
   375  			if p.GetHeight() < memR.mempool.Height()-peerHeightDiff {
   376  				memR.Logger.Debug("peer is too far behind us. Skipping broadcast of seen tx")
   377  				continue
   378  			}
   379  		}
   380  		// no need to send a seen tx message to a peer that already
   381  		// has that tx.
   382  		if memR.mempool.seenByPeersSet.Has(txKey, id) {
   383  			continue
   384  		}
   385  
   386  		p2p.SendEnvelopeShim(peer, p2p.Envelope{ //nolint: staticcheck
   387  			ChannelID: MempoolStateChannel,
   388  			Message:   msg,
   389  		}, memR.Logger)
   390  	}
   391  }
   392  
   393  // broadcastNewTx broadcast new transaction to all peers unless we are already sure they have seen the tx.
   394  func (memR *Reactor) broadcastNewTx(wtx *wrappedTx) {
   395  	msg := &protomem.Message{
   396  		Sum: &protomem.Message_Txs{
   397  			Txs: &protomem.Txs{
   398  				Txs: [][]byte{wtx.tx},
   399  			},
   400  		},
   401  	}
   402  
   403  	for id, peer := range memR.ids.GetAll() {
   404  		if p, ok := peer.Get(types.PeerStateKey).(PeerState); ok {
   405  			// make sure peer isn't too far behind. This can happen
   406  			// if the peer is blocksyncing still and catching up
   407  			// in which case we just skip sending the transaction
   408  			if p.GetHeight() < wtx.height-peerHeightDiff {
   409  				memR.Logger.Debug("peer is too far behind us. Skipping broadcast of seen tx")
   410  				continue
   411  			}
   412  		}
   413  
   414  		if memR.mempool.seenByPeersSet.Has(wtx.key, id) {
   415  			continue
   416  		}
   417  
   418  		p2p.SendEnvelopeShim(peer, p2p.Envelope{ //nolint: staticcheck
   419  			ChannelID: mempool.MempoolChannel,
   420  			Message:   msg,
   421  		}, memR.Logger)
   422  	}
   423  }
   424  
   425  // requestTx requests a transaction from a peer and tracks it,
   426  // requesting it from another peer if the first peer does not respond.
   427  func (memR *Reactor) requestTx(txKey types.TxKey, peer p2p.Peer) {
   428  	if peer == nil {
   429  		// we have disconnected from the peer
   430  		return
   431  	}
   432  	memR.Logger.Debug("requesting tx", "txKey", txKey, "peerID", peer.ID())
   433  	msg := &protomem.Message{
   434  		Sum: &protomem.Message_WantTx{
   435  			WantTx: &protomem.WantTx{TxKey: txKey[:]},
   436  		},
   437  	}
   438  
   439  	success := p2p.SendEnvelopeShim(peer, p2p.Envelope{ //nolint: staticcheck
   440  		ChannelID: MempoolStateChannel,
   441  		Message:   msg,
   442  	}, memR.Logger)
   443  	if success {
   444  		memR.mempool.metrics.RequestedTxs.Add(1)
   445  		requested := memR.requests.Add(txKey, memR.ids.GetIDForPeer(peer.ID()), memR.findNewPeerToRequestTx)
   446  		if !requested {
   447  			memR.Logger.Error("have already marked a tx as requested", "txKey", txKey, "peerID", peer.ID())
   448  		}
   449  	}
   450  }
   451  
   452  // findNewPeerToSendTx finds a new peer that has already seen the transaction to
   453  // request a transaction from.
   454  func (memR *Reactor) findNewPeerToRequestTx(txKey types.TxKey) {
   455  	// ensure that we are connected to peers
   456  	if memR.ids.Len() == 0 {
   457  		return
   458  	}
   459  
   460  	// pop the next peer in the list of remaining peers that have seen the tx
   461  	// and does not already have an outbound request for that tx
   462  	seenMap := memR.mempool.seenByPeersSet.Get(txKey)
   463  	var peerID uint16
   464  	for possiblePeer := range seenMap {
   465  		if !memR.requests.Has(possiblePeer, txKey) {
   466  			peerID = possiblePeer
   467  			break
   468  		}
   469  	}
   470  
   471  	if peerID == 0 {
   472  		// No other free peer has the transaction we are looking for.
   473  		// We give up 🤷‍♂️ and hope either a peer responds late or the tx
   474  		// is gossiped again
   475  		memR.Logger.Info("no other peer has the tx we are looking for", "txKey", txKey)
   476  		return
   477  	}
   478  	peer := memR.ids.GetPeer(peerID)
   479  	if peer == nil {
   480  		// we disconnected from that peer, retry again until we exhaust the list
   481  		memR.findNewPeerToRequestTx(txKey)
   482  	} else {
   483  		memR.mempool.metrics.RerequestedTxs.Add(1)
   484  		memR.requestTx(txKey, peer)
   485  	}
   486  }