github.com/aakash4dev/cometbft@v0.38.2/spec/p2p/reactor-api/reactor.qnt (about) 1 // -*- mode: Bluespec; -*- 2 /* 3 * Reactor is responsible for handling incoming messages on one or more 4 * Channel. Switch calls GetChannels when reactor is added to it. When a new 5 * peer joins our node, InitPeer and AddPeer are called. RemovePeer is called 6 * when the peer is stopped. Receive is called when a message is received on a 7 * channel associated with this reactor. 8 */ 9 // Code: https://github.com/aakash4dev/cometbft/blob/main/p2p/base_reactor.go 10 module reactor { 11 12 // Unique ID of a node. 13 type NodeID = str 14 15 /* 16 * Peer is an interface representing a peer connected on a reactor. 17 */ 18 type Peer = { 19 ID: NodeID, 20 21 // Other fields can be added to represent the p2p operation. 22 } 23 24 // Byte ID used by channels, must be globally unique. 25 type Byte = str 26 27 // Channel configuration. 28 type ChannelDescriptor = { 29 ID: Byte, 30 Priority: int, 31 } 32 33 /* 34 * Envelope contains a message with sender routing info. 35 */ 36 type Envelope = { 37 Src: Peer, // Sender 38 Message: str, // Payload 39 ChannelID: Byte, 40 } 41 42 // A Routine is used to interact with an active Peer. 43 type Routine = { 44 name: str, 45 peer: Peer, 46 } 47 48 type ReactorState = { 49 // Peers that have been initialized but not yet removed. 50 // The reactor should expect receiving messages from them. 51 peers: Set[NodeID], 52 53 // The reactor runs multiple routines. 54 routines: Set[Routine], 55 56 // Values: init -> registered -> running -> stopped 57 state: str, 58 59 // Name with which the reactor was registered. 60 name: str, 61 62 // Channels the reactor is responsible for. 63 channels: Set[ChannelDescriptor], 64 } 65 66 // Produces a new, uninitialized reactor. 67 pure def NewReactor(): ReactorState = { 68 { 69 peers: Set(), 70 routines: Set(), 71 state: "init", 72 name: "", 73 channels: Set(), 74 } 75 } 76 77 // Pure definitions below represent the `p2p.Reactor` interface methods: 78 79 /* 80 * GetChannels returns the list of MConnection.ChannelDescriptor. Make sure 81 * that each ID is unique across all the reactors added to the switch. 82 */ 83 pure def GetChannels(s: ReactorState): Set[ChannelDescriptor] = { 84 s.channels // Static list, configured at initialization. 85 } 86 87 /* 88 * SetSwitch allows setting a switch. 89 */ 90 pure def SetSwitch(s: ReactorState, switch: bool): ReactorState = { 91 s.with("state", "registered") 92 } 93 94 /* 95 * Start the service. 96 * If it's already started or stopped, will return an error. 97 */ 98 pure def OnStart(s: ReactorState): ReactorState = { 99 // Startup procedures should come here. 100 s.with("state", "running") 101 } 102 103 /* 104 * Stop the service. 105 * If it's already stopped, will return an error. 106 */ 107 pure def OnStop(s: ReactorState): ReactorState = { 108 // Shutdown procedures should come here. 109 s.with("state", "stopped") 110 } 111 112 /* 113 * InitPeer is called by the switch before the peer is started. Use it to 114 * initialize data for the peer (e.g. peer state). 115 */ 116 pure def InitPeer(s: ReactorState, peer: Peer): (ReactorState, Peer) = { 117 // This method can update the received peer, which is returned. 118 val updatedPeer = peer 119 (s.with("peers", s.peers.union(Set(peer.ID))), updatedPeer) 120 } 121 122 /* 123 * AddPeer is called by the switch after the peer is added and successfully 124 * started. Use it to start goroutines communicating with the peer. 125 */ 126 pure def AddPeer(s: ReactorState, peer: Peer): ReactorState = { 127 // This method can be used to start routines to handle the peer. 128 // Below an example of an arbitrary 'ioRoutine' routine. 129 val startedRoutines = Set( {name: "ioRoutine", peer: peer} ) 130 s.with("routines", s.routines.union(startedRoutines)) 131 } 132 133 /* 134 * RemovePeer is called by the switch when the peer is stopped (due to error 135 * or other reason). 136 */ 137 pure def RemovePeer(s: ReactorState, peer: Peer, reason: str): ReactorState = { 138 // This method should stop routines created by `AddPeer(Peer)`. 139 val stoppedRoutines = s.routines.filter(r => r.peer.ID == peer.ID) 140 s.with("peers", s.peers.exclude(Set(peer.ID))) 141 .with("routines", s.routines.exclude(stoppedRoutines)) 142 } 143 144 /* 145 * Receive is called by the switch when an envelope is received from any connected 146 * peer on any of the channels registered by the reactor. 147 */ 148 pure def Receive(s: ReactorState, e: Envelope): ReactorState = { 149 // This method should process the message payload: e.Message. 150 s 151 } 152 153 // Global state 154 155 // Reactors are uniquely identified by their names. 156 var reactors: str -> ReactorState 157 158 // Reactor (name) assigned to each channel ID. 159 var reactorsByCh: Byte -> str 160 161 // Helper action to (only) update the state of a given reactor. 162 action updateReactorTo(reactor: ReactorState): bool = all { 163 reactors' = reactors.set(reactor.name, reactor), 164 reactorsByCh' = reactorsByCh 165 } 166 167 // State transitions performed by the p2p layer, invoking `p2p.Reactor` methods: 168 169 // Code: Switch.AddReactor(name string, reactor Reactor) 170 action register(name: str, reactor: ReactorState): bool = all { 171 reactor.state == "init", 172 // Assign the reactor as responsible for its channel IDs, which 173 // should not be already assigned to another reactor. 174 val chIDs = reactor.GetChannels().map(c => c.ID) 175 all { 176 size(chIDs.intersect(reactorsByCh.keys())) == 0, 177 reactorsByCh' = reactorsByCh.keys().union(chIDs). 178 mapBy(id => if (id.in(chIDs)) name 179 else reactorsByCh.get(id)), 180 }, 181 // Register the reactor by its name, which must be unique. 182 not(name.in(reactors.keys())), 183 reactors' = reactors.put(name, 184 reactor.SetSwitch(true).with("name", name)) 185 } 186 187 // Code: Switch.OnStart() 188 action start(reactor: ReactorState): bool = all { 189 reactor.state == "registered", 190 updateReactorTo(reactor.OnStart()) 191 } 192 193 // Code: Switch.addPeer(p Peer): preamble 194 action initPeer(reactor: ReactorState, peer: Peer): bool = all { 195 reactor.state == "running", 196 not(peer.ID.in(reactor.peers)), 197 updateReactorTo(reactor.InitPeer(peer)._1) 198 } 199 200 // Code: Switch.addPeer(p Peer): conclusion 201 action addPeer(reactor: ReactorState, peer: Peer): bool = all { 202 reactor.state == "running", 203 peer.ID.in(reactor.peers), // InitPeer(peer) and not RemovePeer(peer) 204 reactor.routines.filter(r => r.peer.ID == peer.ID).size() == 0, 205 updateReactorTo(reactor.AddPeer(peer)) 206 } 207 208 // Code: Switch.stopAndRemovePeer(peer Peer, reason interface{}) 209 action removePeer(reactor: ReactorState, peer: Peer, reason: str): bool = all { 210 reactor.state == "running", 211 peer.ID.in(reactor.peers), // InitPeer(peer) and not RemovePeer(peer) 212 // Routines might not be started, namely: not AddPeer(peer) 213 // Routines could also be already stopped if Peer has erroed. 214 updateReactorTo(reactor.RemovePeer(peer, reason)) 215 } 216 217 // Code: Peer type, onReceive := func(chID byte, msgBytes []byte) 218 action receive(reactor: ReactorState, e: Envelope): bool = all { 219 reactor.state == "running", 220 // The message's sender is an active peer 221 e.Src.ID.in(reactor.peers), 222 // Reactor is assigned to the message's channel ID 223 e.ChannelID.in(reactorsByCh.keys()), 224 reactorsByCh.get(e.ChannelID) == reactor.name, 225 reactor.GetChannels().exists(c => c.ID == e.ChannelID), 226 updateReactorTo(reactor.Receive(e)) 227 } 228 229 // Code: Switch.OnStop() 230 action stop(reactor: ReactorState): bool = all { 231 reactor.state == "running", 232 // Either no peer was added or all peers were removed 233 reactor.peers.size() == 0, 234 updateReactorTo(reactor.OnStop()) 235 } 236 237 // Simulation support 238 239 action init = all { 240 reactors' = Map(), 241 reactorsByCh' = Map(), 242 } 243 244 // Modelled reactor configuration 245 pure val reactorName = "myReactor" 246 pure val reactorChannels = Set({ID: "3", Priority: 1}, {ID: "7", Priority: 2}) 247 248 // For retro-compatibility: the state of the modelled reactor 249 def state(): ReactorState = { 250 reactors.get(reactorName) 251 } 252 253 pure val samplePeers = Set({ID: "p1"}, {ID: "p3"}) 254 pure val sampleChIDs = Set("1", "3", "7") // ChannelID 1 not registered 255 pure val sampleMsgs = Set("ping", "pong") 256 257 action step = any { 258 register(reactorName, NewReactor.with("channels", reactorChannels)), 259 val reactor = reactors.get(reactorName) 260 any { 261 reactor.start(), 262 reactor.stop(), 263 nondet peer = oneOf(samplePeers) 264 any { 265 // Peer-specific actions 266 reactor.initPeer(peer), 267 reactor.addPeer(peer), 268 reactor.removePeer(peer, "no reason"), 269 reactor.receive({Src: peer, 270 ChannelID: oneOf(sampleChIDs), 271 Message: oneOf(sampleMsgs)}), 272 } 273 } 274 } 275 276 }