Interface IAction
An in-game action. Every action should be replayable, because multiple nodes in a network should execute an action and get the same result.
A “class” which implements this interface is analogous to a function, and its instance is analogous to a partial function application, in other words, a function with some bound arguments. Those parameters that will be bound at runtime should be represented as fields or properties in an action class, and bound argument values to these parameters should be received through a constructor parameters of that class.
From a perspective of security, an action class belongs to the network protocol, and property values in an action belong to a node's will (i.e., a user/player's choice). That means if you define an action class it also defines what every honest node can do in the network. Even if a malicious node changes their own action code it won't affect other honest nodes in the network.
For example, where honest nodes share the common action
Heal(Target) => PreviousStates[Target] + 1
, suppose a malicious
node m
changes their own Heal
action code to
Heal(Target) => PreviousStates[Target] + 2
(2 instead of 1),
and then send an action Heal(m)
.
Fortunately, this action does not work as m
's intention,
because the changed code in itself is not used by other honest nodes,
so they still increase only 1, not 2. The effect of that double healing
is a sort of “illusion” only visible to the malicious node
alone.
In conclusion, action code is a part of the protocol and it works with consensus in the network, so only things each node can affect the network in general is property values of each action they sign and send, not code of an action.
Namespace: Libplanet.Action
Assembly: Libplanet.dll
Syntax
public interface IAction
Examples
The following example shows how to implement an action of three types of in-game logic:
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Bencodex.Types;
using Libplanet;
using Libplanet.Action;
public class MyAction : IAction
{
// Declare an enum type to distinguish types of in-game logic.
public enum ActType { CreateCharacter, Attack, Heal }
// Declare properties (or fields) to store "bound" argument values.
public ActType Type { get; private set; }
public Address TargetAddress { get; private set; }
// Action must has a public parameterless constructor.
// Usually this is used only by Libplanet's internals.
public MyAction() {}
// Take argument values to "bind" through constructor parameters.
public MyAction(ActType type, Address targetAddress)
{
Type = type;
TargetAddress = targetAddress;
}
// The main game logic belongs to here. It takes the
// previous states through its parameter named context,
// and is offered "bound" argument values through
// its own properties (or fields).
IAccountStateDelta IAction.Execute(IActionContext context)
{
// Gets the state immediately before this action is executed.
// ImmutableDictionary<string, uint> is just for example,
// As far as it is serializable, you can store any types.
// (We recommend to use immutable types though.)
var state =
context.PreviousStates.GetState(TargetAddress);
Dictionary dictionary;
// This variable purposes to store the state
// right after this action finishes.
IImmutableDictionary<IKey, IValue> nextState;
// Does different things depending on the action's type.
// This way is against the common principals of programming
// as it is just an example. You could compare this with
// a better example of PolymorphicAction<T> class.
switch (Type)
{
case ActType.CreateCharacter:
if (!TargetAddress.Equals(context.Signer))
throw new Exception(
"TargetAddress of CreateCharacter action " +
"only can be the same address to the " +
"Transaction<T>.Signer.");
else if (!(state is null))
throw new Exception(
"Character was already created.");
nextState = ImmutableDictionary<IKey, IValue>.Empty
.Add((Text)"hp", (Integer)20);
break;
case ActType.Attack:
dictionary = (Bencodex.Types.Dictionary)state;
nextState =
dictionary.SetItem(
(Text)"hp",
(Integer)Math.Max(
dictionary.GetValue<Integer>("hp") - 5,
0)
);
break;
case ActType.Heal:
dictionary = (Bencodex.Types.Dictionary)state;
nextState =
dictionary.SetItem(
(Text)"hp",
(Integer)Math.Min(
dictionary.GetValue<Integer>("hp") + 5,
20)
);
break;
default:
throw new Exception(
"Properties are not properly initialized.");
}
// Builds a delta (dirty) from previous to next states, and
// returns it.
return context.PreviousStates.SetState(TargetAddress,
(Dictionary)nextState);
}
// Serializes its "bound arguments" so that they are transmitted
// over network or stored to the persistent storage.
IValue IAction.PlainValue =>
new Bencodex.Types.Dictionary(new Dictionary<IKey, IValue>
{
[(Text)"type"] = (Integer)(int)Type,
[(Text)"target_address"] = (Binary)TargetAddress.ToByteArray(),
});
// Deserializes "bound arguments". That is, it is inverse
// of PlainValue property.
void IAction.LoadPlainValue(
IValue plainValue)
{
var dictionary = (Bencodex.Types.Dictionary)plainValue;
Type = (ActType)(int)dictionary.GetValue<Integer>("type");
TargetAddress =
new Address(dictionary.GetValue<Binary>("target_address"));
}
}
Note that the above example has several bad practices. Compare this example with PolymorphicAction<T>'s example.
Properties
| Improve this Doc View SourcePlainValue
Serializes values bound to an action, which is held by properties (or fields) of an action, so that they can be transmitted over network or saved to persistent storage.
Serialized values are deserialized by LoadPlainValue(IValue) method later.
Declaration
IValue PlainValue { get; }
Property Value
Type | Description |
---|---|
IValue | A Bencodex value which encodes this action's bound values (held by properties or fields). |
See Also
Methods
| Improve this Doc View SourceExecute(IActionContext)
Executes the main game logic of an action. This should be deterministic.
Through the context
object,
it receives information such as a transaction signer,
its states immediately before the execution,
and a deterministic random seed.
Other “bound” information resides in the action object in itself, as its properties (or fields).
A returned IAccountStateDelta object functions as a delta which shifts from previous states to next states.
Declaration
IAccountStateDelta Execute(IActionContext context)
Parameters
Type | Name | Description |
---|---|---|
IActionContext | context | A context object containing addresses that signed the transaction, states immediately before the execution, and a PRNG object which produces deterministic random numbers. See IActionContext for details. |
Returns
Type | Description |
---|---|
IAccountStateDelta | A map of changed states (so-called "dirty"). |
Remarks
This method should be deterministic: for structurally (member-wise) equal actions and IActionContexts, the same result should be returned. Side effects should be avoided, because an action's Execute(IActionContext) method can be called more than once, the time it's called is difficult to predict.
For changing in-memory game states or drawing graphics, implement the IRenderer<T> interface separately and attach it to a BlockChain<T> instance.
For randomness, never use Random nor any other PRNGs provided by other than Libplanet. Use Random instead. Random guarantees the same action has the consistent result for every node in the network.
Also do not perform I/O operations such as file system access or networking. These bring an action indeterministic. You maybe fine to log messages for debugging purpose, but equivalent messages could be logged multiple times.
Although it might be surprising, floating-point arithmetics are underspecified so that it can make different results on different machines, platforms, runtimes, compilers, and builds.
Lastly, you need to be aware and keep in mind that there is a global state named CurrentCulture on .NET; if you format numbers, dates and times, currencies, or other such things into strings and parse these strings back these can rely on CurrentCulture, so that the same action make different results on two differently configured systems like Thai language and French language. In order to make these types of conversions deterministic, you have to explicitly pass InvariantCulture.
For more on determinism in general, please read also Tendermint ABCI's docs on determinism.
Lastly, you can conduct static analysis on your code using Libplanet.Analyzers. The analyzer can be enabled by adding its NuGet package into your project as a dependency.
See Also
| Improve this Doc View SourceLoadPlainValue(IValue)
Deserializes serialized data (i.e., data PlainValue property made), and then fills this action's properties (or fields) with the deserialized values.
Declaration
void LoadPlainValue(IValue plainValue)
Parameters
Type | Name | Description |
---|---|---|
IValue | plainValue | Data (made by PlainValue property) to be deserialized and assigned to this action's properties (or fields). |