In this blog post, we will proceed with the Game Example that we have seen in the last aticle. We will refactor the code to eliminate the redundant logic and optimize code using the concept of State Machines in Plutus.
1. Important concepts related to the State Machine with Plutus
State Machine: Represented by the UTXO sitting at the State Machine address.
State: Represented by the datum of the UTXO sitting at the State Machine address.
Transition: It’s the transaction that consumes the current UTXO using a redeemer that characterizes the transition. In our game, we have written the redeemer based on the players’ choices.
To link the three concepts together, we can say that the transaction representing the state transition produces a UTXO sitting at the same script address, but with a datum carrying the new state. The UTXO carrying the current state is identified by a specific NFT.
2. State Machine Implementation with Plutus
We can find details about the state machine implementation in the Plutus documentation, more exactly in the Plutus.Contract.StateMachine Module.
Figure 1: State machine implementation in Plutus documentation.
2.1. State Machine Inputs
As cited in the documentation, the state machine has two inputs:
S (State), which corresponds to the datum type;
I (Input), which corresponds to the redeemer type.
2.2. State Machine Related Functions
Now, we will take a look at the four functions of the state machine in Plutus:
- Function1: “smTransition”
This function represents a transition in a state machine. Practically, given a state “s” and a redeemer “i,” we can either get nothing if the transition fails (not allowed) or, if the transition succeeds, we return a Tuple.
Indeed, the smTransition returns “s,” which represents the new state, is composed of the value of the UTXO generated by our transition transaction and the corresponding new datum.
Figure 2: Representation of the State in the Plutus documentation.
Our smTransition also returns TxConstraints, which are additional constraints that the transition transaction should satisfy.
- Function 2 : “smFinal”
This function returns a boolean indicating whether the last transition is final or not.
If the state is final, the state machine stops and doesn’t generate a new UTXO.
- Function 3 : “smCheck”
This function is similar to the smTransition function, but returns a boolean as a result of a transaction check.
This function allows us to apply further checks to the state transition (transaction) that are not covered by the TxConstraints in the smTransition function.
As described in the documentation, the default implementation of this function is always returning a true value.
- Function 4 : “smThreadToken”
We have previously mentioned that the transition function must carry an NFT (called Thread Token) in order to distinguish the UTXO sitting at the state machine address carrying the current state from other UTXOs sitting at the same address, which are obviously not relevant to us.
This function allows us to automatically mint and handle this NFT.
3. Implementation of the Game On-chain Code with a State Machine
Now, we will return to the code representing our game to refactor it based on the concepts of the State Machine.
3.1. The Game Type
Figure 3: Implementation of the Game Type in the Case of State Machine
First, we begin with the game type. The main difference compared to the previous implementation without a state machine is that we have now changed the type of the “gToken” (which represents the NFT distinguishing the UTXO carrying the actual state from other UTXOs sitting at the same address) from “AssetClass” to “ThreadToken” in order to conform with state machine functions, as discussed previously.
3.2. The Game Datum Type
The second difference is that we added a “Finished” constructor for the state “GameDatum”, which represents the final state of the state machine, which is, of course, the final state of the game.
Figure 4: Implementation of the Game Datum in the Case of State Machine.
3.3. “transition” Function vs. “mkGameValidator” Function
The third and main difference in the state machine code occurs in the “transition” function of the state machine, carrying the core business logic of our game, which replaced the “mkGameValidator” that we have previously seen.
Figure 5: Implementation of the Transition function with State Machine
Here are the main differences and similarities between the “transition” function (case with state machine implementation) and the “mkGameValidator” function (case without state machine implementation):
- The code of the transition function is considerably shorter than the mkGameValidator;
- The signatures of the two functions are different. In the transition function, we have to check whether the combination of the transaction with the datum and the redeemer is valid or not;
- An advantage with the state machine is that we don’t have to check if the UTXO that we are consuming carries the NFT used as an identifier. This will be done automatically by the state machine;
- The state machine transition function will check automatically if the state is final or not;
- In the state machine’s case, we don’t need the helper functions “ownInput” and “ownOutput” to validate the values of the UTXOs, previously used in the implementation without a state machine;
- In the transition function, we cannot check the nonce revealed by the first player using a constraint, so we have to declare that in a separate function. This function will be called “check” (see Figure 16);
- We must declare the final state (“Finished”) separately in a function that returns the true given a GameDatum(state) corresponding to the final state, and returning false otherwise.
Figure 6: Comparison between parts of the mkGameValidator function (implementation without a state machine) with the corresponding parts in the transition function (implementation with a state machine).
3.4. gameStateMachine Function
In this step, we can define the State Machine function that we will call “gameStateMachine.”
Our state machine function takes as input the:
Final state check function;
Nonce check function;
Thread Token (NFT) check function.
3.5. gameStateMachine Function
Finally, we can declare the “mkGameValidator”, which, in this case, will take the state machine function as input.
Figure 7: Implementation of the nonce check function, gameStateMachine function, and mkGameValidator function.
3.6. State Machine Client: The gameClient
Additionally, in order to be able to interact with the state machine from the off-chain code (from a wallet), one of the main differences is that we have to use a StateMachine Client called “gameClient”.
Figure 8: Representation of the State Machine Client in the Plutus documentation.
4. Implementation of the Game Off-chain Code with a State Machine:
Now, we will move to our off-chain code, which is shorter than the implementation without a state machine because our state machine simplifies the thread token (NFT) handling and provides a bunch of functions to retrieve the current state (getOnChainState function).
Figure 9: Representation of the getOnChainState function.
Another important state machine function that makes the code more compact is the runStep function, which allows automatic state transitioning by submitting transactions.
Figure 10: Representation of the runStep function.
After completing the off-chain code, we can test the game and simulate its execution using the emulator trace, which will obviously return the same results as those previously presented in the implementation without a state machine.
5. Other Interesting State Machine Implementation Use Case: Token Sale Example
An excellent example of where to use the state machine is the case of a token sale. So Lars presented the example and some of the possible states of this use case and implemented it using the state machine.
You can check the details of the implementation in Lecture 8 of the Plutus Pioneer program under this link.
State machines are a very important concept that we should use, when it’s appropriate to do so, to reduce the amount of code and eliminate code duplications in the off-chain, as well as in the on-chain code. A shorter code means a more readable code and better quality.
Indeed, the mechanism of identifying the UTXO carrying the current state and handling the NFT identifying this UTXO is handled automatically by the state machine.
Nevertheless, it’s also worth mentioning that contracts written using state machines are resources hungry compared to contracts written without state machines. In fact, more resources are required to run these types of validators and minting policies. That’s why this concept is not yet widely used in practice, but the Plutus development team is working on optimizing State Machines with Plutus.
Support also our PeakChain Automotive Solutions in Project Catalyst Fund 9!