 Hello, this is Matej from Consensus Lab in Perco Labs, and the second video in the series on MIR, our framework for implementing distributed systems. This time, we will be looking at actual coding using MIR, and namely using a ping-pong little toy application that will first model on paper, and then we will show how to code it using the MIR framework by writing actual code. So let's get to it right away. In the introduction, in the previous video, you saw what distributed systems model we consider, what is a node, what is an algorithm, what is message passing, how event-based abstractions work, and how we use them to model distributed systems and protocols. And you also saw a very high-level overview of MIR itself, namely what it is, how it works, and how a distributed system is modeled using MIR. If you did not see the video yet, you can click on this button that will get you to watch the first video. So now I assume you did watch it. If you didn't, go watch it and come back here, and let's continue. So in this video, we will see a toy ping-pong protocol where nodes basically just send ping messages and respond with pong messages, nothing else. And we will also actually write code implementing that protocol using the MIR library. It will be a low-level implementation using rather low-level primitives of MIR just to understand exactly how MIR works. And in future videos, we will see how this can be actually simplified because a big part of the code that I will be showing can now actually be already generated automatically from just the definitions of the events and messages. So let's get to the abstraction itself. We want to implement a ping-pong abstraction where ping messages are received and pong messages are sent. Now, if you remember from the previous video, this is what it should look like, right? There's a ping event coming in the ping-pong abstraction and a pong event going out. Well, if you really think hard about it and remember properly how we modeled it in the previous video, this is actually not the case. Why is it not the case? Because we are receiving ping messages and sending pong messages. We don't really define ping and pong events, which when we're discussing protocols can be usually used interchangeably. But when we want to implement it and we want to thoroughly modulate, this is actually different. So what is it in reality? In reality, we have the ping-pong abstraction and it just receives ping messages. So it consumes events, and the event is message received, namely message ping from some node. And it emits events, there are send message events, and these messages are also parameterized by a pong type and by a node to which the message should go. All right, so this is the core of our abstraction that we want to have and we will have some implementation inside that will decide what to do on the message reception and when the messages should be sent. Now if you want to implement it, we need to put together some system that can actually work. So we need a network transport abstraction that can actually execute the sending and receiving and it will consume the send message events and produce the message receive events. And we also want to start sending ping messages somehow because for now we're only reacting to pings by sending pongs, but we don't send pings from anywhere. So one way to do it is, for example, using some timer abstraction and we can tell it using a repeat event that it should periodically, with period that's specified in the parameter, in the parameter, that it should be emitting some event that we specify. And then it will be periodically triggering that event back to us. This is already implemented in Mir out of the box as well as the network transport protocol. So we only need to worry about implementing the ping pong module, which is our actual protocol. So how do we implement the protocol? Well, this is the protocol of our protocol, which is actually a rather low level protocol. As I explained before, we are just reacting to events. Automatically in Mir, every module gets the init event and there's also very common specifying distributed abstraction implementations that each module can count on the init event in the start and Mir implements it that way as well. So at the start, we set our counter to zero and we will be counting our ping messages and we set up the timer. So here we trigger an event that will go to the timer module, basically go to the timer abstraction. It will be the timer repeat event that the timer abstraction understands. And the repeat event will be parameterized by some event that we define. Let's call it ping time here with a period of one second. And in our in our abstraction that we already have implemented in Mir, this event is actually understood by the timer module. So, OK, so we know what to do on init. What do we do when the ping time event actually arrives from the timer? Well, then we create another event. It will be destined to the net module, the networking module. The event itself will be send message event and the parameters of the send message event are two, namely the ping message with the current counter value and the destination will be the other node. Like if we have two nodes and we will be we'll be doing it for two nodes. If we'll be not ourselves, but the other node and we increment the counter. When we receive a ping message, we will need to do something. So the event itself is message received and parameterized with the message and with the node that it came from. And then the logic needs to check what type the message has. Either it's a ping message, in which case we just print something to the console and respond with a Pong message to the very same node the message was received from. If it's a Pong message, we just print the sequence number to the console and do nothing else. This is the protocol logic that we will need to be executing and implementing. So what will it look like in terms of a mere node? In the last video, you saw what a mere node looks like. So this is a particular instance of a mere node and that will result from what we will be building. We will have a mere node with its event buffering, then dispatcher and everything provided by mere and we will put three modules in it, namely the net module, ping pong module and a timer module. The net and the timer module are already implemented in our library, so we can just use them and we will only need to implement the ping pong module. All right, so let's start coding. We're going to create the ping pong application completely from scratch. And that means I set up a completely fresh virtual machine that I'm going to log into now. And it's a fresh Ubuntu 22.04 Linux. It has nothing installed on it except for the SSH server and Git. And I shared some folders with my host computer so I can use the IDE to edit the files and the IDE is running on the host. But otherwise, it's completely out of the box Ubuntu machine. All right, so let's clone the repository in the shared folder I have. Which is here. For that, I go to the repository website. I clone the code, copy this, a bit long, so I have my copy of the repository. And then I just go and follow the instructions on compiling and running tests. All right, so here it says we should have go install. So I just go and install go, OK. And then I install some of the other dependencies as said on the repository website. And then download it. Now I just need to add the go binary path to my profile so I can use these install things. And then I log out, I log in. OK, I'm back in the mere directory. And now let's start writing some code. So for that, let me just open an IDE window with the repository. All right, here we go. And let's put the sample ping pong application in the samples directory as another sample. Let's call it ping pong. Let's create a main file, OK. And it's a main function that just prints something like starting ping pong. All right, let's try to run it and see what it runs. I already prepared two terminal windows where we will be running the two nodes that are ping ponging. So let's just SSH, we're going to know M, very nice. Let's use this one for now. And we go to the directory with mere and we go run samples ping pong. Not a main package. Yes, I need to call it main. And it's starting ping pong. Very good. OK, so for the example, we'll have two nodes that are ping ponging. So let's define the network addresses. For the network communication, we will use the included GRPC communication module. And that module takes input to the membership of the system, which will just hard code for the two nodes for this example. And we give these addresses to the module. So let's say addresses are just a map from node ID to address. The types node ID and the address are defined by the mere package already. And let's create just two addresses there. The addresses are actually multi addresses, lipid to p-style multi addresses. So let's say address node zero. Let's be the first node, the ID zero, multi address, new multi address. Why is it red? Because we need to use an error and this can return OK. And it's an IPv4 address with local host. So when it's seven, zero, zero, one on PCP and let's use just 10,000. All right. And well, let's not do too much error handling in the example. But let's just always panic on error. Yes. OK. So let's say that the address of the first node, the address of the second node with ID one will be the same. They will both run on the local host and let's say on different ports. OK. We created the addresses. And this data structure will need to just go. We'll just need to give to the network module. So let's say let's call it transport. Transport and error is we have a gRPC package, gRPC, new transport. And this creates directly a transport module that we can use. OK. The node ID needs the node ID, the addresses and some logger. So what will be the own ID? Let's take the own ID from the command line. So on ID is OS.Rx1 and its type is node ID. So we get node ID. The next parameter is, oh, sorry, own ID. The next parameter are the addresses. And then for logging, we also use the built-in console logger. So logging console, let's just log the warnings. The logging package is also a sub-package of a mirror. OK. There's a problem with the addresses. Because we only need to give it the own address. So the address of ID. Very good. OK. So now we need to, OK, let's check the error. And we panic on error. Now we need to start listening. We need to tell the transport to start listening. And if it returns an error, we panic. So starting the transport means that the ports will be opened, and we can start receiving messages from other nodes. And now we need to still connect to other nodes. So transport.connect the other nodes. And yes, we need to give it a context. So let's just give it a background. And we give it the two addresses that we need to connect to, including the own address. All right. So now we basically created a network module for two nodes that MIR will be able to use. Now let us create the MIR node. So the MIR node, we created using the function mir.newnodes, and we're going to compare it as well. mir.newnode. And now what arguments do we need to give the node? We need to give it the ID of the node, which is the own ID. Yes. Then we need to configure it. And yes, we can give it the configuration, which is basically just let's use the default configuration mir.default node config. OK. Now what else do we need to give the node? Now we need to give it the modules. So the modules is a map of module ID to actual module. And the only module we created so far is a transport module. And we can call it transport. So we give the node will only contain the transport module for now. OK. And then it needs to get the write and log on the interceptor, which we are not using. So we just say new. I forgot a comma. Good. So this is how we create a node in mir. There's an error we panicked. And when we create a node, we need to start it. We need to run it. So we say node.run with some context. Well, let's use the background context. And this is a blocking function that will block until the node stops running. And it returns an error. And actually, let's put it there. So since this is a blocking function, we need to put it in a separate go routine so we can continue executing. So we say go.tun. We put it inside. And let's just create a channel for the error to be written to. That's so we have access to it when the node finishes. All right, we can say here that the mir node is running. And we can have it run for, I know, for 10 seconds, just as an example. And then we stop it. When we call node stop, this will make the run function return. The error will be written. And we can read the error and print it out. The error, in this case, will not be nil. It is a specific mir defined error which says that the node shut down on request. So it is not nil. And we still can print it. So mir error is this. And I need to print that in a new line. All right, let's run it for five seconds. And let's try to run this. Now, we create the transport. We create the node. We run the node. And then we stop it after five seconds. So let's see what happens. We run the ping pong. First time, it needs download a bunch of libraries and compile. There's some problem because I have a wrong format of the multi-address. Another problem, because now I need to give the node its own ID, so its own ID will be 0. And here we go. We're starting ping pong. And the node doesn't start. Why doesn't the node start? Because here the transport needs to first connect to all the other nodes. And we don't have the other node running. So let's run the other node. So we run the same thing, but with ID 1. I happen to have something like this in this directory as well, but I actually didn't SSH to my virtual machine. So SSH near my node em, I got the mere directory. And now I run it with ID 1. This mirror node is running now. And we'll finish in five seconds. This guy didn't even manage to try to reconnect. So let's try to run them at the same time. Now both nodes are running. After five seconds, they shut down. And they said the error is stopped at caller request, exactly. All right, so we have a node up and running. It connects to the other node, and it does nothing. So now it's time to start implementing our ping pong protocol, is what the nodes will actually do. So for that, let's create a new file. Let's call it ping pong. The ping pong protocol will be implemented as a passive module. And the passive module is just a module that implements one function, namely apply events, as we saw in the presentation. So let's say it's type ping pong is a structure that will represent our module. And we implement the passive module interface. We generate code, implement methods, passive module. Yes, and we make it a pointer one. OK, so this implements module function is just a dummy function that says that this is actually a module. We don't need to implement anything inside there. And the only thing we actually need to implement is applying events. So here, the apply events function receives a list of events and returns another list of events and another. Let's rename this to something else, because events is also the name of the package with the events. And we already have a convenience function in Mir in the modules package where we specify just the handler function, and it will apply this function to all the events. So we return only the modules apply events sequentially. There's one for applying them concurrently as well. And we give it the events. And we need to give it a function that takes a single event and returns an event list. For that, let's implement it also as a method of the ping pong. So here, we have our apply event function that doesn't take a list of events but a single event, which is a protobuf generated object and returns an event list. So we pass the function here. And now we need to start defining how to apply events. So what events are we going to apply? Well, our function, let me show you the pseudocode again, we have an init function, we have a ping time, we have an init event, we have a ping time event, and we have a message receive event. Let's try for the start, just ignore the timer. And let's define our ping and pong messages. So in order to define a message, we need to go to the protoss directory because all the protobufs are defined here and the messages are also expressed as protobufs. And let's create another protobuf directory called ping pong pb ping pong protobuf with a file inside that will define the messages ping pong pb protobuf. Yes, add it to the repository, whatever. OK, so it's a protophile. So let me just write the header of it. Cynx is protobuf 3 package will be ping pong pb. And the go package will be github.com filecoin project near. This is our code base. And we put in the pkg slash pb for protobufs in ping pong pb because all the protobufs are assembled in pb. These are the protobufs. And those are generated when we run the generator. OK, so now let's define our messages. We have a ping message and a pong message. So let's say message ping. And let's give a payload, some dummy payload to each ping message, like the sequence number, for example. Let's, yeah, I mean, you in 64. And this will be something that the message carries. And the pong message will, let's say, it will just return the same sequence number of the ping message. So there's a pong message with some sequence number. OK, now both these messages will be a subtype of a ping pong message. So let's say we create a message and we call it message. And it will be, it will only have one one of field. Let's call it type, just convention for historical reasons. This is how we usually do it in mirror. And it will be one of ping or pong. So the ping message is a ping pong message. And the ping ping message is a message. And the pong message is a message. Now this message, we need to register it with the mere messages. To this end, we go to the proto file with the message. Some dependencies are not loaded by the IDE yet, but it's OK. And we just add another type of message, namely, we say ping pong pb, a proto actually ping pong pb. It's kind of a ping pong pb.message. This will be a ping pong message number file. OK, now that we registered the ping pong message type with the general message type, we only need to tell the protobuf compiler to also use it for generating the code. And for that, we just add it to the generate.go file and say ping pong pb slash pong pb.proto. I think we are ready to compile the code, to compile the protobufs. OK, there is some problem here because I have a typo somewhere. Yes, I have typo here. So I need to rename this to ping pong pb. Very nice. Now it's generating the first time in Brunsit needs to download some dependencies. OK, so we have our ping pong messages ready. And they're registered with ping pong with the main message type. So we can go back to our module implementation. And now we can already use the message types we have. So let's see what we need to do to implement the module. So we receive an event, and this function needs to handle, needs to basically say what to do when handling the event. There are many different event types. So depending on the type, we decide what to do. So I go switch based on the event type. E is event.type type. And first thing we wanted to do was the init event. And let me be consistent here. So we return the result of an apply init function. We define in a function what happens from the apply init. On apply init, let's say we do nothing for now. We just return an empty event list and no error, because we have nothing to initialize so far. OK, the next case, it can be a message received event, event pb, event message received. And if the event is a message received event, then we just handle the message using a function apply message received, ping pong, apply message received. And what it receives is the received message. So here, if the event type is message received, the actual type is stored in the E variable. And in the message received, this is how protobus work. And this event, we get as an argument to the message received application function. OK, now we need to decide what to do with the received message. And we decide based on the message type. So we actually do something, we don't do nothing. And we say, switch message dot message dot type. And the only kind of message the ping pong module will be handling will be a ping pong message, as we defined it in the protobus. And only the ping pong message gen will have two subtypes called ping and pong. So we have one case type like this. So case message eb, message ping pong, that the only message type will handle. We'll do something. And by default, we just complain that we don't know the message type. Unknown message type of this type. Very good. All right, so now when we have the ping pong message type, we have another switch statement distinguishing the ping and the pong message. So we write switch. And this is message dot ping pong dot type. And here we have two cases, one for ping, one for pong. So let's say message ping is one. And this message pong is the other one. And we always call the corresponding handler function that we only write here. And we define it later. So apply ping with message dot ping, why not? And ascender. And it's a node ID. Why don't we have the ascender? Because it's both from. All right, the same goes for the pong message. And this will be the pong message. And now we can define the handlers. OK, let's see what the co-pilot suggests to us. We get the ping and the from. Oh, it's pretty smart. And we do nothing for now, let's say. And the other one will be the pong message doing nothing. OK, so we have the skeleton. And now we decide what to do on the reception of the ping message. Well, let's say we print a message saying that we receive the ping. So fmp.print, and then receive ping message. What we want to do on the reception of the ping message is to respond with a pong. OK, now we need to create the pong message. I could start creating the protocol buffer here directly, but I prefer having a constructor function that is much less ugly and much less cluttering the code here. So I'll make another sub-package here called something like whatever protobus or that are specific for the ping pong module. I created a file called protobus.go. Here I'll just write some helper functions to create the protobus I need. Now, here I will be writing them manually. But in fact, in the newest version of Mir, this can be actually generated by a co-generator. So you won't have to write it by hand. But it's a nice exercise to understand how this actually works. So first, let me create a function that returns a general message from a ping pong message. So it's in the ping pong protobus. And I need to define this module, which is a type of module ID, well, and the message itself. It's a ping pong specific message. And it returns a general message that will contain the ping pong message. So let's see what the co-pilot suggested to us. Yes, we create a protobuf general message. The destination module is a destination module. We give it as an argument. The PB method just returns a protobuf specific representation of this custom type. And it has this one parameter, which is type. And it is the ping pong message type. And it contains the ping pong message itself. So this is how protobufers work in Go. And now we can create two more functions with the specific ping and specific pong message. So firstly, we create a ping message. And we give it the destination module and a sequence number because we defined the message to have a sequence number. And what we return is a message, a general message. And we here not have to just create the protocol buffer for a ping pong message. Let's see what the suggested code works. So the destination module is just given in the argument. And here we just construct a protocol buffer representation of ping pong message with the type ping. And the content will be a ping message with a given sequence number. Very nice. And now we do the very same thing for the pong message. Pong message has a destination, some sequence number. And we create a message with type pong. And this will be pong. And the pong message will be the content. So this is just boilerplate code. And again, this can actually be generated. I just wrote it by hand here to demonstrate how it works. All right, so now we have functions that will return our messages. So we can go back to our protocol logic. So on the reception of the ping message, we say we received a ping message from some node. And we now create a pong message. It's just protobufs.pong message. OK, now the destination module is our module. And our module is called ping pong. This is the destination module on the destination node where the message should be routed. And the sequence number will be the same as the sequence number of the ping message we receive, ping.sequence number. And we get the ping message in the argument. OK, now we have the pong message. So we need to create an event that would send the message. So send message event, send message event. So it's events.sendMessage. And now the event for sending a message doesn't go to the ping pong module. It goes locally to the transport module because this is a local event in our node. And it says that the pong message should be sent. So it goes to the transport module. And the message is the pong message. And the destination, there will be only one destination. But it still needs to be a slice of destinations. So we create a slice of node IDs. And from is the ID of the node we receive it from. OK, and we return a list of events that contains only this single event to send the message. All right, now what happens on the other side when we actually receive the message? Because eventually this function will be called on the other side, on the other node. So let's say we just do nothing. We only print that we receive the pong message. Print it like this, both of the messages. And we'll print a node ID as string. And here as well, then we'll add it at the end. OK, so now we know what to do when we receive a ping message. We know what to do when we receive a pong message. Actually, the pong message we don't even use. So let's actually add the sequence number of the message here and here so we know which of the ping and pong messages we receive. Now, if we run this, nothing will actually happen because there's nothing to trigger the sending of the ping message in the first place. So let's do just for testing now that each node on initialization will actually send one ping message. So I can copy the code here because it's going to be similar. So instead of doing nothing at the init event, we create a ping message. We add a module with some sequence number. OK, what will be the sequence number? Let's say sequence number 0 for now because we're the only one. We create the send message event. And to which node are we going to send the message now? We need to send it to the other node. What's the other node? Well, let's see. The other node is 1 if our ID is 0 and the other node is 0 if our ID is 1. We don't know, however, here what our ID is. So let's make that a property of the ping pong of the state of the ping pong module. Own ID is node ID. And when we instantiate the ping pong module, we will pass it our own ID. And now we can say this ID equals, let's say our best ID of type node ID. And if our own ID 0, then best ID will be 1. Otherwise, the best ID will be 0. OK, so we send the message to the destination ID, the node with the destination ID. And we return the send message event on initialization. OK, let's try to run it. I'm curious what it's going to work. We run here with ID 0. There's a problem ping pong.go33. No missing return. Yeah, we don't have the return statement here because we only have two cases. What happens in it and what happens a message received. So let's just simply add a default case that will simply complain that we don't know what to do with the event. OK, the co-pilot suggestion is actually pretty good. Unown event type, and we say what type it is. All right, in line 66, I guess it was a similar problem. Exactly, we have a default statement here. If it's a ping pong message, we do something. If it's not a ping pong message, we complain. But within the ping pong message switch, we only have the ping and the pong. So let's add a default case where we also say the unknown ping pong message type. And it will be not the general but the type of message m. OK, and in front here, we have it nicer. We can also just say message. OK, let's try to run it now. Seems that it started running near node. Nothing happened. OK, you probably guessed it when nothing happened. We have this beautiful ping pong module that we just implemented, but we don't use it. We still just create a node that only has the transport. So let's add the ping pong module to the node. And let's just create a ping pong module directly with own ID being our own ID. And let's try to run it again. Very nice. So each node received one ping message and one pong message from the other node. But this was only one message, one ping message and one response pong message. So let us try to have the nodes and ping messages periodically, and we can use this opportunity to see how the timer module works. Because for this, we'll need the timer module. All right. So what the timer module does is just sit there and wait until it gets events. They will ask it to set some timeouts, either periodic timeouts or one of timeouts. We will be using a periodic timeout. For this, basically the timeout receives an event saying it should set up a periodic timeout. And this event contains another event. And with the defined period, the timer will generate this event that is enclosed in the timeout request. So for this, we will need to define our timeout event. So we'll go to our protobuf definition, which is here. And as we have messages, we'll have to define events as well as protobufs. So first, we define a ping-pong-type event. So we say message event. And we're in the ping-pong package. So this will be the ping-pong event. And it will also be a one-off type. And we'll only have one event for now, which will be, let's call it the ping-time. Like, whenever there's time to send a ping message, it will create a ping-time event. Ping-time event, which is ping equals one. Now, we just need to define the ping-time event. Ping-time, actually, it can be a completely empty event because we always know what to do with that event. So we defined the event like this. Now, we need to regenerate the protobufs, which happens here. And from now on, we can use the generated protobuf. Now, we can also add a constructor for that. So we don't have to write the ugly protobuf-related code. So let's first write a function that creates a ping-pong-type event. And so it will be event because we're in the ping-pong package. Let's see what it proposes to us. It has a destination module as a parameter and the actual ping-pong event. And it returns an event, bb.event. And it returns an event with the given destination module. Again, the module ID represented as a protobuf type. And the type is event bb, event ping-pong. Why doesn't the ping-pong event get the normal color? Because we haven't registered the ping-pong event among the events. So we need to go to the event bb and register the ping-pong event as an event type, the same as we did for the message. Now, there's only messages and event. There's only two things we need to register. So it's not like every time you create something new, you would need to go and register it. You would need to do it once for events and once for messages. So we'd say import bb, ping-pong.b.proto. And we add another event. That is the ping-pong event type of type ping-pong.event. OK, let's regenerate the protobuf code. Very good. And let's hope it helped. In our protobufs event, the ping-pong event itself is the ping-pong event. So this is just a wrapper of the ping-pong event type. And now we can create one more function to create the actual ping-time event. So let's say ping-time event for destination module returns an event with the same destination module. And the event itself will be a ping-pong event containing the ping-time event with nothing inside. All right. And now we are ready to use the timer. So how do we use the timer? Well, let's say that at initialization, instead of sending a ping-pong message, we just set up the timer to send the ping-pong messages. So maybe we can create a function called send ping. But we'll do exactly this, send ping. And it will return just an event list and an error just for consistency with the rest of the code. And the apply in it, instead of sending the ping message, we'll now set up the timer. How do we set up the timer? We create a timer event. So timer event will be events.timerRepeat. The timer repeat event is already a mere supported event. For the timer, we can actually go and see the implementation of the timer module. And we see in its apply events method, it accepts, apart from the init event, the timer delay event, the timer repeat event, and the timer garbage collect. You can have a look at it later. For now, we don't need the delay and the garbage collect. We only need the timer repeat event. So we set up the timer repeat event. So it needs a destination module. The destination module will be the timer module. Now we need an event that will be sent repeatedly to us or not necessarily to us. Now we need to specify an event that will repeatedly be triggered. And actually, it works for multiple events. We only need one, but it's still defined as a slice. So we need to create a slice of events and we'll create a product or bus ping time event. Now this ping time event will need to be routed back to us. And our module is the ping pong module. And one more parameter we need to give, two more parameters we need to give to the timer event is the delay with which we want the timer to occur. So let's say it will be one second. And let me put it on separate lines. And one more parameter, which is what we call the retention index, has to do with garbage collection. I don't want to go into details with this. But basically, this will allow us to cancel this periodic producing of the events. And we have one problem here, because we have a custom time duration type in a mirror to abstract time duration away. So we need to cast it for this because the underlying type now is the native time duration. OK, so we have the timer event created. And what we need to do now is only return the event. So it's actually emitted from the apply init function. So we emit a list of events with the timer event and nothing else. Now we just need to tell the module what to do when the ping time event occurs, when it's actually produced by the timer. For that, we go back to the apply event handler and we add another case clause or a ping pong event. Because the event emitted by the timer will be a ping pong event, as we defined before. So event PB dot event ping pong. And now we just need to make sure that the ping pong event is actually the ping time event and not some other event. Well, it cannot really be in our case because you only defined the ping time event as a subtype of ping pong. But nevertheless, we at least need to assert the type. So we say switch and E is the ping pong type type. And we just look for a ping pong PB dot event ping time. In that case, we just return and let's be consistent. We just return p dot apply ping time with the received event. We will define this function later. In general, even the apply event function technically could be generated. So if we are consistent in our writing of the code, this is just a boilerplate that we will not eventually need to be writing anymore. OK, we add a default statement for completeness, just complaining that it's an unknown ping pong event that we receive. And now we just need to apply the ping time event. So let's say function p of ping pong will apply ping time with a ping time event. And let's say we say, well, the only thing that we need to do when the time of the curse is basically return the send ping. So whenever the ping time event occurs, we don't really need to come in here. We just send the ping message. OK, the last thing we need to do for this to work is to actually include the timer in our node system. So we give the module the ID timer. And we create the timer module from the mere libraries provided code, which is timer dot new. And this will create a new mere timer that implements what we need. So let's try to run it. And now we're getting repeatedly ping and pong messages, which looks good. All of them have the sequence number 0. So let's make it slightly more interesting to give them some actual sequence numbers, which means that we just go to the ping pong module implementation, and we add the counter for our ping messages. And each time we send a ping message, we increment this counter. So we can say that the next SN, let's make it a U in 64, because this will be defined in the protobuf anyway. And it will be initialized to 0 automatically. So we don't even need to add anything to the init, but we can just for it to be explicit 0. And each time we send a ping message, we actually give it the P dot next SN. And we increment the counter. OK, so let's try to run the code. When two fuel values is track literal. Yes, this is because I was sloppy. And when I was creating the ping pong module, I just created an ad hoc ping pong object. So let's actually create a proper clean constructor, saying new ping pong with own ID. And let's define it properly. And this would return a new ping pong module. So function new ping pong. And let's export it just for it to be clean ping pong. And it will return pointer. It needs to be pointer because the module is an interface with the own ID being the own ID. And next SN being 0 just for completeness, although Go would do this automatically. OK, so let's try to run this now. Very nice. So now for the five seconds, we are sending a ping message every second and then we receive a pong response. OK, so this is our simple ping pong module. It keeps sending ping messages and the nodes are responding with pong messages. And this was demonstrate how mirror can be used to implement a protocol. On purpose, this was done at a very low level. So this was a low level mirror programming tutorial where all the code I wrote by hand. And this is not the case usually. I will show in the next videos how most of this code can be generated by using something like a domain specific language that mirror supports. For this, we have a DSL module that already has most of the boilerplate code. Some of the boilerplate code can also be generated. So we'll basically only need to write the actual logic, namely the implementations of apply ping, apply pong, apply init, and so on. And so we'll basically need to define our events and messages. The rest of the boilerplate code, for example, the constructors that we were writing by hand now, they will be automatically generated. And then we will use other constructions within the mirror library that would only require us to write the actual logic. One other video, and this will be the very next video, will be about how to use the active modules because the ping pong module is a passive module. It only reacts to messages and events. So the active module will actually be injecting things. So for this, we will get rid of the timer. And instead of periodically sending the ping message every second, we will actually have the user decide when a ping message has to be sent. And we will use an active module to inject that event into mirror and send the message like that.