(image)

Programming Hotmoka
A tutorial on Hotmoka and smart contracts in Takamaka

3.4 Calling a method on an object in a Hotmoka node

(See the io-hotmoka-tutorial-examples-family-exported and the io-hotmoka-tutorial-examples-runs projects in https://github.com/Hotmoka/hotmoka)

In the previous section, we have created an object of class Person in the store of the node. Let us invoke the toString() method on that object now. For that, we can use the moka objects call command, specifying our ‘Person‘ object as receiver.

In object-oriented languages, the receiver of a call to a non-static method is the object over which the method is executed, that is accessible as this inside the code of the method. In our case, we want to invoke einstein.toString(), where einstein is the object that we have created previously, hence the receiver of the call. The receiver can be seen as an implicit actual argument passed to a (non-static) method.

moka objects call 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#0 io.hotmoka.tutorial.examples.family.Person toString --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=3
bb5bdb7126f7a3de5dc9cd3f9ad66fe6a85152a418c3fb86add9053e553292d#0
Adding transaction 95c241d054c15c5f12f4a7867664b9891ff308625d71b310db2346bedeba38cb... rejected.
The transaction failed with message Class io.hotmoka.tutorial.examples.family.Person of the parameter 3bb5bdb7126f7a3de5dc9cd3f9ad66fe6a85152a418c3fb86add9053e553292d#0 is not exported: add @Exported to io.hotmoka.tutorial.examples.family
.Person

Command moka objects call requires to specify, as its first arguments, the payer of the call, that is, our account, followed by the name of the class whose method is called and the name of that method. The receiver of the call is specified through --receiver.

As you can see above, the result is deceiving.

This exception occurs when we try to pass the Person object as receiver of toString() (the receiver is a particular case of an actual argument). That object has been created in store, has escaped the node and is available through its storage reference 3bb5bdb7126f7a3d…#0. However, it cannot be passed back into the node as argument of a call since it is not exported. This is a security feature of Hotmoka. Its reason is that the store of a node is public and can be read freely. Everybody can see the objects created in the store of a Hotmoka node and their storage references can be used to invoke their methods and modify their state. This is true also for objects meant to be private state components of other objects and that are not expected to be freely modifiable from outside the node. Because of this, Hotmoka requires that classes, whose instances can be passed into the node as arguments to methods or constructors, must be annotated as @Exported. This means that the programmer acknowledges the use of these instances from outside the node.

Note that all objects can be passed, from inside the blockchain, as arguments to methods of code in the node. The above limitation applies to objects passed from outside the node only.

Let us modify the Person class again:

...
import io.takamaka.code.lang.Exported;
...


@Exported
public class Person extends Storage {
    ...
}

Package the project io-hotmoka-tutorial-examples-family:

cd io-takamaka-code-examples-family
mvn clean install
cd ..
moka jars install 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#0 io-hotmoka-tutorial-examples-family/target/io-hotmoka-tutorial-examples-family-1.11.5.jar --yes --password-of-payer --uri=ws://panarea.hotmoka.io:8001
Enter value for --password-of-payer (the password of the key pair of the payer account): chocolate
The jar has been installed at c65ae21be4a3a529fe75a0699c4c022b01f651efd7c05852c1aed757cf907f83.


Gas consumption:
 * total: 497592
   * for CPU: 15369
   * for RAM: 5183
   * for storage: 477040
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 497592 panas

then create a new Person object:

moka objects create 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#0 io.hotmoka.tutorial.examples.family.Person Einstein 14 4 1879 null null --classpath=c65ae21be4a3a529fe75a0699c4c022b01f651efd7c05852c1aed757cf907f83 --
password-of-payer --uri=ws://panarea.hotmoka.io:8001
Enter value for --password-of-payer (the password of the key pair of the payer account): chocolate
Do you really want to call constructor
  public ...Person(java.lang.String,int,int,int,...Person,...Person)
  spending up to 1000000 gas units at the price of 1 pana per unit (that is, up to 1000000 panas) [Y/N] Y
A new object f93da63e00dbbbd586cb04d9e474eeabdd7f682896453dd098f54d3e949b41cd#0 has been created.


Gas consumption:
 * total: 58708
   * for CPU: 14951
   * for RAM: 5037
   * for storage: 38720
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 58708 panas

and finally call toString() on that new object:

moka objects call 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#0 io.hotmoka.tutorial.examples.family.Person toString --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=
f93da63e00dbbbd586cb04d9e474eeabdd7f682896453dd098f54d3e949b41cd#0
Adding transaction 8641f30ce7a1d5534adb59cc33d83c0326b54afd56080517038e2de57daea884... done.
The method returned:
Einstein (14/4/1879)


Gas consumption:
 * total: 48817
   * for CPU: 15948
   * for RAM: 7109
   * for storage: 25760
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 48817 panas

This time, the correct answer Einstein (14/4/1879) appears on the screen.

In Ethereum, the only objects that can be passed, from outside the blockchain, as argument to method calls into blockchain are contracts. Namely, in Solidity it is possible to pass such objects as their untyped address that can only be cast to contract classes. Takamaka allows more, since any object can be passed as argument, not only contracts, as long as its class is annotated as @Exported. This includes all contracts since the class io.takamaka.code.lang.Contract, that we will present later, is annotated as @Exported and @Exported is an inherited Java annotation.

We can do the same in code, instead of using the moka objects call command. Namely, we can expand the FamilyStorage class seen before in order to run a further transaction, that calls toString(). For that, copy then the following FamilyExported class inside the package io.hotmoka.tutorial.examples.runs of the io-hotmoka-tutorial-examples-runs project:

package io.hotmoka.tutorial.examples.runs;


import static io.hotmoka.helpers.Coin.panarea;
import static io.hotmoka.node.StorageTypes.INT;
import static java.math.BigInteger.ONE;


import java.math.BigInteger;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;


import io.hotmoka.constants.Constants;
import io.hotmoka.crypto.api.Signer;
import io.hotmoka.helpers.GasHelpers;
import io.hotmoka.helpers.SignatureHelpers;
import io.hotmoka.node.Accounts;
import io.hotmoka.node.ConstructorSignatures;
import io.hotmoka.node.MethodSignatures;
import io.hotmoka.node.StorageTypes;
import io.hotmoka.node.StorageValues;
import io.hotmoka.node.TransactionRequests;
import io.hotmoka.node.api.Node;
import io.hotmoka.node.api.requests.SignedTransactionRequest;
import io.hotmoka.node.api.transactions.TransactionReference;
import io.hotmoka.node.api.types.ClassType;
import io.hotmoka.node.api.values.StorageReference;
import io.hotmoka.node.api.values.StorageValue;
import io.hotmoka.node.remote.RemoteNodes;


public class FamilyExported {


    private final static ClassType PERSON = StorageTypes.classNamed
        ("io.hotmoka.tutorial.examples.family.Person");


    public static void main(String[] args) throws Exception {


        // the path of the user jar to install
        var familyPath = Paths.get(System.getProperty("user.home")
        + "/.m2/repository/io/hotmoka/io-hotmoka-tutorial-examples-family/"
        + Constants.HOTMOKA_VERSION
        + "/io-hotmoka-tutorial-examples-family-" + Constants.HOTMOKA_VERSION + ".jar");


        var dir = Paths.get(args[1]);
        var payer = StorageValues.reference(args[2]);
        var password = args[3];


        try (var node = RemoteNodes.of(new URI(args[0]), 150000)) {
            // we get a reference to where io-takamaka-code-X.Y.Z.jar has been stored
            TransactionReference takamakaCode = node.getTakamakaCode();


            // we get the signing algorithm to use for requests
            var signature = node.getConfig().getSignatureForRequests();


            KeyPair keys = loadKeys(node, dir, payer, password);


            // we create a signer that signs with the private key of our account
            Signer<SignedTransactionRequest<?>> signer = signature.getSigner
            (keys.getPrivate(), SignedTransactionRequest::toByteArrayWithoutSignature);


            // we get the nonce of our account: we use the account itself as caller and
            // an arbitrary nonce (ZERO in the code) since we are running
            // a @View method of the account
            BigInteger nonce = node
            .runInstanceMethodCallTransaction(TransactionRequests.instanceViewMethodCall
            (payer, // payer
            BigInteger.valueOf(100_000), // gas limit
            takamakaCode, // class path for the execution of the transaction
            MethodSignatures.NONCE, // method
            payer)).get() // receiver of the method call
            .asBigInteger(__ -> new ClassCastException());


            // we get the chain identifier of the network
            String chainId = node.getConfig().getChainId();


            var gasHelper = GasHelpers.of(node);


            // we install the family jar in the node: our account will pay
            TransactionReference family = node
            .addJarStoreTransaction(TransactionRequests.jarStore
            (signer, // an object that signs with the payer's private key
            payer, // payer
            nonce, // payer's nonce: relevant since this is not a call to a @View method!
            chainId, // chain identifier: relevant since this is not a call to a @View method!
            BigInteger.valueOf(1_000_000), // gas limit: enough for this small jar
            gasHelper.getSafeGasPrice(), // gas price: at least the current gas price of the network
            takamakaCode, // class path for the execution of the transaction
            Files.readAllBytes(familyPath), // bytes of the jar to install
            takamakaCode)); // dependencies of the jar that is being installed


            // we increase our copy of the nonce, ready for further
            // transactions having the account as payer
            nonce = nonce.add(ONE);


            // call the constructor of Person and store in einstein the new object in blockchain
            StorageReference einstein = node.addConstructorCallTransaction
            (TransactionRequests.constructorCall
            (signer, // an object that signs with the payer's private key
            payer, // payer
            nonce, // payer's nonce: relevant since this is not a call to a @View method!
            chainId, // chain identifier: relevant since this is not a call to a @View method!
            BigInteger.valueOf(300_000), // gas limit: enough for a small object
            panarea(gasHelper.getSafeGasPrice()), // gas price, in panareas
            family, // class path for the execution of the transaction


            // constructor Person(String,int,int,int)
            ConstructorSignatures.of(PERSON, StorageTypes.STRING, INT, INT, INT),


            // actual arguments
            StorageValues.stringOf("Einstein"), StorageValues.intOf(14),
            StorageValues.intOf(4), StorageValues.intOf(1879)
            ));


            // we increase our copy of the nonce, ready for further
            // transactions having the account as payer
            nonce = nonce.add(ONE);


            StorageValue s = node.addInstanceMethodCallTransaction
            (TransactionRequests.instanceMethodCall
            (signer, // an object that signs with the payer's private key
            payer, // payer
            nonce, // payer's nonce: relevant since this is not a call to a @View method!
            chainId, // chain identifier: relevant since this is not a call to a @View method!
            BigInteger.valueOf(100_000), // gas limit: enough for a simple call
            panarea(gasHelper.getSafeGasPrice()), // gas price, in panareas
            family, // class path for the execution of the transaction


            // method to call: String Person.toString()
            MethodSignatures.ofNonVoid(PERSON, "toString", StorageTypes.STRING),


            // receiver of the method to call
            einstein
            )).get();


            // we increase our copy of the nonce, ready for further
            // transactions having the account as payer
            nonce = nonce.add(ONE);


            // print the result of the call
            System.out.println(s);
        }
    }


    private static KeyPair loadKeys(Node node, Path dir, StorageReference account, String password) throws Exception {
        return Accounts.of(account, dir)
        .keys(password, SignatureHelpers.of(node).signatureAlgorithmFor(account));
    }
}

The interesting part is the call to addInstanceMethodCallTransaction() at the end of the previous listing. It requires to resolve method Person.toString() by using einstein as receiver (the type StorageTypes.STRING is the return type of the method) and to run the resolved method. It stores the result in s, that subsequently prints on the standard output. You can run class FamilyExported. You will obtain the same result as with moka objects call:

mvn compile exec:java -Dexec.mainClass="io.hotmoka.tutorial.examples.runs.FamilyExported" -Dexec.args="ws://panarea.hotmoka.io:8001 hotmoka_tutorial 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#0 chocolate"
Einstein (14/4/1879)

As we have shown, method addInstanceMethodCallTransaction() can be used to invoke an instance method on an object in the store of the node. This requires some clarification. First of all, note that the signature of the method to call is resolved and the resolved method is then invoked. If such resolved method is not found (for instance, if we tried to call tostring() instead of toString()), then addInstanceMethodCallTransaction() would end up in a failed transaction. Moreover, the usual resolution mechanism of Java methods applies. If, for instance, we invoked MethodSignatures.ofNonVoid(StorageTypes.OBJECT, "toString", StorageTypes.STRING) instead of MethodSignatures.ofNonVoid(PERSON, "toString", StorageTypes.STRING), then method toString() would have be resolved from the run-time class of einstein, looking for the most specific implementation of toString(), up to the java.lang.Object class, which would anyway end up in running Person.toString().

Method addInstanceMethodCallTransaction() can be used to invoke instance methods with parameters. If a toString(int) method existed in class Person, then we could call it and pass 2019 as its argument, by writing:

StorageValue s = node.addInstanceMethodCallTransaction
(TransactionRequests.instanceMethodCall(
...


// method to call: String Person.toString(int)
MethodSignatures.ofNonVoid(PERSON, "toString", StorageTypes.STRING, StorageTypes.INT),


// receiver of the method to call
einstein,


// actual argument(s)
StorageValues.intOf(2019)
)).get();

where we have added the formal parameter StorageTypes.INT and the corresponding actual argument StorageValues.intOf(2019). Note that method calls yields an optional value in Hotmoka, since methods returning void actually return non value. Consequently, we need the final call to get() above to get the actual value returned by the method.

Method addInstanceMethodCallTransaction() cannot be used to call a static method. For that, use addStaticMethodCallTransaction() instead, that accepts a request similar to that for addInstanceMethodCallTransaction(), but without a receiver.