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 }