(image)

Programming Hotmoka
A tutorial on Hotmoka and smart contracts in Takamaka

Chapter 5 The support library

This chapter presents the support library of the Takamaka language, that contains classes for simplifying the definition of smart contracts.

In Sec. 3.5, we said that storage objects must obey to some constraints. The strongest of them is that their fields of reference type, in turn, can only hold storage objects. In particular, arrays are not allowed there. This can be problematic, in particular for contracts that deal with a dynamic, variable, potentially unbound number of other contracts.

Therefore, most classes of the support library deal with such constraints, by providing fixed or variable-sized collections that can be used in storage objects, since they are storage objects themselves. Such utility classes implement lists, arrays and maps and are consequently generally described as collections. They have the property of being storage classes, hence their instances can be kept in the store of a Hotmoka node, as long as only storage objects are added as elements of the collection. As usual with collections, these utility classes have generic type, to implement collections of arbitrary, but fixed types. This is not problematic, since Java (and hence Takamaka) allows generic types.

5.1 Storage lists

Lists are an ordered sequence of elements. In a list, it is typically possible to access (read or write) the first element in constant time, while accesses to the nth element require to scan the list from its head and consequently have a cost proportional to n. Because of this, lists are not, in general, random-access data structures, whose nth element should be accessible in constant time. The size of a list is not fixed: lists grow in size as more elements are added.

(-tikz- diagram)

Figure 5.1: The hierarchy of storage lists.

Java has many classes for implementing lists, all subclasses of java.util.List. They cannot be used in Takamaka that, instead, provides an implementation of lists with the storage class io.takamaka.code.util.StorageLinkedList. Its instances are storage objects and can consequently be held in fields of storage classes and can be stored in a Hotmoka node, as long as only storage objects are added to the list. Takamaka lists provide constant-time access and addition to both ends of a list. We refer to the JavaDoc of StorageLinkedList for a full description of its methods. They include methods for adding elements to either end of the list, for accessing and removing elements, for iterating on a list and for building a Java array E[] holding the elements of a list.

Fig. 5.1 shows the hierarchy of the storage lists. A storage list implements the interface StorageList, that defines the methods that modify a list. That interface extends the interface StorageListView that, instead, defines the methods that read data from a list, but do not modify it. This distinction between the read-only interface and the modification interface is typical of all collection classes in the Takamaka library, as we will see. For the moment, note that this distinction is useful for defining methods snapshot() and view(). Both return a StorageListView but there is an important difference between them. Namely, snapshot() yields a frozen view of the list, that cannot and will never be modified, also if the original list gets subsequently updated. Instead, view() yields a view of a list, that is, a read-only list that changes whenever the original list changes and exactly in the same way: if an element is added to the original list, the same automatically occurs to the view. In this sense, a view is just a read-only alias of the original list. Both methods can be useful to export data, safely, from a node to the outside world, since both methods return an @Exported object without modification methods. Method snapshot() runs in linear time (in the length of the list) while method view() runs in constant time.

Differently from the other collection classes that we will describe in this chapter, the snapshot() method of lists runs in linear time on the length of the list. This is inherently related to the structure of a linked list, that requires a full copy in order to create an unmodifiable clone. Other collections, later, will instead allow the creation of snapshots in constant time.

It might seem that view() is just an upwards cast to the interface StorageListView. This is wrong, since that method does much more. Namely, it applies the façade design pattern to provide a distinct list that lacks any modification method and implements a façade of the original list. To appreciate the difference to a cast, assume to have a StorageList list and to write

StorageListView<E> view = (StorageListView<E>) list;

This upwards cast will always succeed. Variable view does not allow to call any modification method, since they are not in its static type StorageListView. But a downwards cast back to StorageList is enough to circumvent that constraint: StorageList list2 = (StorageList) view. This way, the original list can be modified by modifying list2 and it would not be safe to export view, since it is a Trojan horse for the modification of list. With method view(), the problem does not arise, since the cast StorageList list2 = (StorageList) list.view() fails: method view() actually returns another list object without modification methods. The same is true for method snapshot() that, moreover, yields a frozen view of the original list. These same considerations hold for the other Takamaka collections that we will see in this chapter.

Next section shows an example of use for StorageLinkedList.

5.1.1 A gradual Ponzi contract

(See the io-hotmoka-tutorial-examples-tictactoe project in https://github.com/Hotmoka/hotmoka)

Consider our previous Ponzi contract from Ch. 4 It is somehow irrealistic, since an investor gets its investment back in full. In a more realistic scenario, the investor will receive the investment back gradually, as soon as new investors arrive. This is more complex to program, since the Ponzi contract must take note of all investors that invested up to now, not just of the current one as in SimplePonzi.java. This requires a list of investors, of unbounded size. An implementation of this gradual Ponzi contract is reported below and has been inspired by a similar Ethereum contract from Iyer and Dannen, shown at page 150 of [11]. Write its code inside package io.hotmoka.tutorial.examples.ponzi of the io-hotmoka-tutorial-examples-ponzi project, as a new class GradualPonzi.java:

package io.hotmoka.tutorial.examples.ponzi;


import static io.takamaka.code.lang.Takamaka.require;


import java.math.BigInteger;


import io.takamaka.code.lang.Contract;
import io.takamaka.code.lang.FromContract;
import io.takamaka.code.lang.Payable;
import io.takamaka.code.lang.PayableContract;
import io.takamaka.code.lang.StringSupport;
import io.takamaka.code.math.BigIntegerSupport;
import io.takamaka.code.util.StorageLinkedList;
import io.takamaka.code.util.StorageList;


public class GradualPonzi extends Contract {
    public final BigInteger MINIMUM_INVESTMENT = BigInteger.valueOf(1_000L);


    /**
        * All investors up to now. This list might contain the same investor many
        * times, which is important to pay him back more than investors
        * who only invested once.
        */
    private final StorageList<PayableContract> investors = new StorageLinkedList<>();


    public @FromContract(PayableContract.class) GradualPonzi() {
         investors.add((PayableContract) caller());
    }


    public @Payable @FromContract(PayableContract.class) void invest(BigInteger amount) {
         // new investments must be at least 10% greater than current
         require(BigIntegerSupport.compareTo(amount, MINIMUM_INVESTMENT) >= 0,
             () -> StringSupport.concat("you must invest at least ", MINIMUM_INVESTMENT));
         BigInteger eachInvestorGets = BigIntegerSupport.divide
             (amount, BigInteger.valueOf(investors.size()));
         investors.forEach(investor -> investor.receive(eachInvestorGets));
         investors.add((PayableContract) caller());
    }
}

The constructor of GradualPonzi is annotated as @FromContract. Therefore, it can only be called by a contract and the latter that gets added, as first investor, inside the field investors, of type io.takamaka.code.util.StorageLinkedList. This list, that implements an unbounded list of objects, is a storage object, as long as only storage objects are added inside it. PayableContracts are storage objects, hence its use is correct here. Subsequently, other contracts can invest by calling method invest(). A minimum investment is required, but this remains constant over time. The amount invested gets split by the number of the previous investors and sent back to each of them. Note that Takamaka allows programmers to use Java’s lambdas. Old fashioned Java programmers, who don’t feel at home with such treats, can exploit the fact that storage lists are iterable and replace the single-line forEach() call with a more traditional (but gas-hungrier):

for (PayableContract investor: investors)
  investor.receive(eachInvestorGets);

It is instead highly discouraged to iterate the list as if it were an array. Namely, do not write

for (int pos = 0; pos <investors.size(); pos++)
  investors.get(i).receive(eachInvestorGets);

since linked lists are not random-access data structures and the complexity of the last loop is quadratic in the size of the list. This is not a novelty: the same occurs with many traditional Java lists that do not implement java.util.RandomAccess (such as java.util.LinkedList). In Takamaka, code execution costs gas and computational complexity does matter, more than in other programming contexts.

5.1.2 A note on re-entrancy

The GradualPonzi.java class pays back previous investors immediately: as soon as a new investor invests something, his investment gets split and forwarded to all previous investors. This should make Solidity programmers uncomfortable, since the same approach, in Solidity, might lead to the infamous re-entrancy attack, when the contract that receives his investment back has a fallback function redefined in such a way to re-enter the paying contract and re-execute the distribution of the investment. As it is well known, such an attack has made some people rich and other desperate. You can find more detail in [2]. Even if such a frightening scenario does not occur, paying back previous investors immediately is discouraged in Solidity also for other reasons. Namely, the contract that receives his investment back might have a redefined fallback function that consumes too much gas or does not terminate. This would hang the loop that pays back previous investors, actually locking the money inside the GradualPonzi contract. Moreover, paying back a contract is a relatively expensive operation in Solidity, even if the fallback function is not redefined, and this cost is paid by the new investor that called invest(), in terms of gas. The cost is linear in the number of investors that must be paid back.

As a solution to these problems, Solidity programmers do not pay previous investors back immediately, but let the GradualPonzi contract take note of the balance of each investor, through a map. This map is updated as soon as a new investor arrives, by increasing the balance of every previous investor. The cost of updating the balances is still linear in the number of previous investors, but it is cheaper (in Solidity) than sending money back to each of them, which requires expensive inter-contract calls that trigger new sub-transactions. With this technique, previous investors are now required to withdraw their balance explicitly and voluntarily, through a call to some function, typically called widthdraw(). This leads to the withdrawal pattern, widely used for writing Solidity contracts.

We have not used the withdrawal pattern in GradualPonzi.java. In general, there is no need for such pattern in Takamaka, at least not for simple contracts like GradualPonzi.java. The reason is that the receive() method of a payable contract (corresponding to the fallback function of Solidity) are final in Takamaka and very cheap in terms of gas. In particular, inter-contract calls are not especially expensive in Takamaka, since they are just a method invocation in Java bytecode (one bytecode instruction). They are not inner transactions. They are actually cheaper than updating a map of balances. Moreover, avoiding the withdraw() transactions reduces the overall number of transactions; without using the map supporting the withdrawal pattern, Takamaka contracts consume less gas and less storage. Hence, the withdrawal pattern is both useless in Takamaka and more expensive than paying back previous contracts immediately.

5.1.3 Running the gradual Ponzi contract

Let us play with the GradualPonzi contract now. We can now start by installing its jar in the node:

cd io-takamaka-code-examples-ponzi
mvn clean install
cd ..
moka jars install 7a73a0a6f4df05153f4bb3e86e04034cd8a8756ce3f9a1e91b9fa4c1c08c52a6#0 io-hotmoka-tutorial-examples-ponzi/target/io-hotmoka-tutorial-examples-ponzi-1.12.1.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 0af05209aba41baff134b5de7e61d8139151bb4c816517717862d28763b23d9e.


Gas consumption:
 * total: 594979
   * for CPU: 15440
   * for RAM: 5219
   * for storage: 574320
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 594979 panas

Create two more keys now, for two more accounts that we are going to create soon:

moka keys create --name=account2.pem --password
Enter value for --password (the password that will be needed later to use the key pair): orange
The new key pair has been written into "account2.pem":
* public key: CFCDDem5K6sAY7WQt4n5bdUQRq2r2iWfgR7xHM9PkTCC (ed25519, base58)
* public key: pxNdCuVOFWGTwNJlZiteINEMQzXETJFp9dzrQ0riuUk= (ed25519, base64)
* Tendermint-like address: 2B05ACFD04F1D758F88E4DDF7F9A5636E58A50F6

and then

moka keys create --name=account3.pem --password
Enter value for --password (the password that will be needed later to use the key pair): apple
The new key pair has been written into "account3.pem":
* public key: HmAftoNSYnC5gf5HyzeTDTQgtMRS1uvnPBrzqRH9MhYC (ed25519, base58)
* public key: +Qt7INkTQwSdYVufJPeqkvyQhNQk6iB5YelLejze61E= (ed25519, base64)
* Tendermint-like address: D13B7A945A444FD173203845D7E55BF167A5D23C

We can create the two new accounts now:

moka accounts create 7a73a0a6f4df05153f4bb3e86e04034cd8a8756ce3f9a1e91b9fa4c1c08c52a6#0 50000000000 account2.pem --password --password-of-payer --uri ws://panarea.hotmoka.io:8001
Enter value for --password (the password of the key pair): orange
Enter value for --password-of-payer (the password of the payer): chocolate
Do you really want to create the new account spending up to 200000 gas units
  at the price of 1 pana per unit (that is, up to 200000 panas) [Y/N] Y
A new account 28168280c59e2aaee09426b4719afee8b35415da4b5b3903177a9d02ccc8758b#0 has been created.
Its key pair has been saved into the file "28168280c59e2aaee09426b4719afee8b35415da4b5b3903177a9d02ccc8758b#0.pem".


Gas consumption:
 * total: 59022
   * for CPU: 15472
   * for RAM: 6590
   * for storage: 36960
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 59022 panas

and

moka accounts create 7a73a0a6f4df05153f4bb3e86e04034cd8a8756ce3f9a1e91b9fa4c1c08c52a6#0 10000000 account3.pem --password --password-of-payer --uri ws://panarea.hotmoka.io:8001
Enter value for --password (the password of the key pair): apple
Enter value for --password-of-payer (the password of the payer): chocolate
Do you really want to create the new account spending up to 200000 gas units
  at the price of 1 pana per unit (that is, up to 200000 panas) [Y/N] Y
A new account d3dc11a8e758a074e5364ac5f66ef923e258a32f766d4bc4041ad0b265dd7977#0 has been created.
Its key pair has been saved into the file "d3dc11a8e758a074e5364ac5f66ef923e258a32f766d4bc4041ad0b265dd7977#0.pem".


Gas consumption:
 * total: 58322
   * for CPU: 15436
   * for RAM: 6566
   * for storage: 36320
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 58322 panas

We let our first account create an instance of GradualPonzi in the node now and become the first investor of the contract:

moka objects create 7a73a0a6f4df05153f4bb3e86e04034cd8a8756ce3f9a1e91b9fa4c1c08c52a6#0 io.hotmoka.tutorial.examples.ponzi.GradualPonzi --classpath=0af05209aba41baff134b5de7e61d8139151bb4c816517717862d28763b23d9e --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 ...GradualPonzi()
  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 7624ebe075ffc3101756cfabe85e23249781f8607ee6244c1778395be0c6a50a#0 has been created.


Gas consumption:
 * total: 66087
   * for CPU: 15154
   * for RAM: 5813
   * for storage: 45120
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 66087 panas

We let the other two players invest, in sequence, in this new GradualPonzi contract. First investment:

moka objects call 28168280c59e2aaee09426b4719afee8b35415da4b5b3903177a9d02ccc8758b#0 io.hotmoka.tutorial.examples.ponzi.GradualPonzi invest 5000 --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=7624
ebe075ffc3101756cfabe85e23249781f8607ee6244c1778395be0c6a50a#0
Enter value for --password-of-payer (the password of the key pair of the payer account): orange
Adding transaction 2b63067890844864397151c17ab5cf9862d16e7bd61b05a4749d29c34cb0b140... done.


Gas consumption:
 * total: 68101
   * for CPU: 16323
   * for RAM: 9538
   * for storage: 42240
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 68101 panas

Second investment:

moka objects call d3dc11a8e758a074e5364ac5f66ef923e258a32f766d4bc4041ad0b265dd7977#0 io.hotmoka.tutorial.examples.ponzi.GradualPonzi invest 15000 --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=7624
ebe075ffc3101756cfabe85e23249781f8607ee6244c1778395be0c6a50a#0
Enter value for --password-of-payer (the password of the key pair of the payer account): apple
Adding transaction 51da48895feebfb6ee233ed239b2b5dce5ee0526429ecbd409f3bdc159395c51... done.


Gas consumption:
 * total: 76012
   * for CPU: 16806
   * for RAM: 11126
   * for storage: 48080
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 76012 panas

We let the first player try to invest again in the contract now, this time with a too small investment, which leads to an exception, since the code of the contract requires a minimum investment:

moka objects call 7a73a0a6f4df05153f4bb3e86e04034cd8a8756ce3f9a1e91b9fa4c1c08c52a6#0 io.hotmoka.tutorial.examples.ponzi.GradualPonzi invest 500 --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=7624
ebe075ffc3101756cfabe85e23249781f8607ee6244c1778395be0c6a50a#0
Enter value for --password-of-payer (the password of the key pair of the payer account): chocolate
Adding transaction 5053c12af65f326d4f96d2072a003bd4eaa9858ee2e1a5f4b9a2e0c425149f55... failed.
The transaction failed with message io.takamaka.code.lang.RequirementViolationException: you must invest at least 1000@GradualPonzi.java:65


Gas consumption:
 * total: 1000000
   * for CPU: 15845
   * for RAM: 7579
   * for storage: 19840
   * for penalty: 956736
 * price per unit: 1 pana
 * total price: 1000000 panas

This exception states that a transaction failed because the last investor invested less than 1000 units of coin. Note that the exception message reports the cause (a require failed) and includes the source program line of the contract where the exception occurred: line \( 65 \) of the source file GradualPonzi.java, that is line

require(BigIntegerSupport.compareTo(amount, MINIMUM_INVESTMENT) >= 0,
  () -> StringSupport.concat("you must invest at least ", MINIMUM_INVESTMENT));

Finally, we can check the state of the contract:

moka objects show 7624ebe075ffc3101756cfabe85e23249781f8607ee6244c1778395be0c6a50a#0 --uri ws://panarea.hotmoka.io:8001
class io.hotmoka.tutorial.examples.ponzi.GradualPonzi (from jar installed at 0af05209aba41baff134b5de7e61d8139151bb4c816517717862d28763b23d9e)
  MINIMUM_INVESTMENT:java.math.BigInteger = 1000
  investors:io.takamaka.code.util.StorageList = 7624ebe075ffc3101756cfabe85e23249781f8607ee6244c1778395be0c6a50a#1
  io.takamaka.code.lang.Contract.balance:java.math.BigInteger = 0

As you can see, the contract keeps no balance. Moreover, its investors field is bound to an object, whose state can be further investigated:

moka objects show 7624ebe075ffc3101756cfabe85e23249781f8607ee6244c1778395be0c6a50a#1 --uri ws://panarea.hotmoka.io:8001
class io.takamaka.code.util.StorageLinkedList (from jar installed at 68e90e2868751a3f22fd5fad3ceef377b91b3e52fdd30705f57505e22902ce40)
  first:io.takamaka.code.util.StorageLinkedList$Node = 7624ebe075ffc3101756cfabe85e23249781f8607ee6244c1778395be0c6a50a#2
  last:io.takamaka.code.util.StorageLinkedList$Node = 51da48895feebfb6ee233ed239b2b5dce5ee0526429ecbd409f3bdc159395c51#0
  size:int = 3

As you can see, it is a StorageLinkedList of size three, since it contains our three accounts that interacted with the GradualPonzi contract instance.