(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 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#0 io-hotmoka-tutorial-examples-ponzi/target/io-hotmoka-tutorial-examples-ponzi-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 09e0121a32bca53d3492834318d68c7dafedf478c8dd92cec549ac3f7ced7295.


Gas consumption:
 * total: 595139
   * for CPU: 15440
   * for RAM: 5219
   * for storage: 574480
   * for penalty: 0
 * price per unit: 1 pana
 * total price: 595139 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: AnbPMKGvmJarn7zNpJtk4DEZrhD31rXaLEfiACi2DHeW (ed25519, base58)
* public key: kWcyyUqbbeY/qoUrYs30R09j3d0QGrYgmGTSRsJNvI8= (ed25519, base64)
* Tendermint-like address: 517CF2D2A1BCF108451436EE06CFB9909BE15AB4

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: Hp6KPmHz9Q6T4jQCRm31nXEGeccuMDt1gaYnTAKzfeRv (ed25519, base58)
* public key: +ctNCXQhfFOGVB+k/YDw6rh6CNraBghPLZJymPkOM9k= (ed25519, base64)
* Tendermint-like address: CE02F6E4E385E7A2F5C98F097B68289432675468

We can create the two new accounts now:

moka accounts create 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#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 5d418519bccfeb6df00c35dc4445f1029a80a96735927b42139856f6bbc0a8c3#0 has been created.
Its key pair has been saved into the file "5d418519bccfeb6df00c35dc4445f1029a80a96735927b42139856f6bbc0a8c3#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 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#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 c3696379cc1b394c6515ae4441b36aa6649d2cfba5dc3390bcaad91ae54c9665#0 has been created.
Its key pair has been saved into the file "c3696379cc1b394c6515ae4441b36aa6649d2cfba5dc3390bcaad91ae54c9665#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 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#0 io.hotmoka.tutorial.examples.ponzi.GradualPonzi --classpath=09e0121a32bca53d3492834318d68c7dafedf478c8dd92cec549ac3f7ced7295 --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 9893c8bd5a40c4ae29f4fc418cd1b46642ff2ab863ccfcb9871bd33d252d1abb#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 5d418519bccfeb6df00c35dc4445f1029a80a96735927b42139856f6bbc0a8c3#0 io.hotmoka.tutorial.examples.ponzi.GradualPonzi invest 5000 --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=9893
c8bd5a40c4ae29f4fc418cd1b46642ff2ab863ccfcb9871bd33d252d1abb#0
Enter value for --password-of-payer (the password of the key pair of the payer account): orange
Adding transaction d17214891066904cd48d50a93883a824665580f12d644aac9f39fe99ad4f137c... 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 c3696379cc1b394c6515ae4441b36aa6649d2cfba5dc3390bcaad91ae54c9665#0 io.hotmoka.tutorial.examples.ponzi.GradualPonzi invest 15000 --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=9893
c8bd5a40c4ae29f4fc418cd1b46642ff2ab863ccfcb9871bd33d252d1abb#0
Enter value for --password-of-payer (the password of the key pair of the payer account): apple
Adding transaction c45bf965d4a90335a633856b959021c8a3608de5265d8e3a7863457383bcec57... 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 3fcbb8889b77be347c3bfe0019683d3fefe2f58712085208c58cfc4d91add793#0 io.hotmoka.tutorial.examples.ponzi.GradualPonzi invest 500 --uri=ws://panarea.hotmoka.io:8001 --password-of-payer --receiver=9893
c8bd5a40c4ae29f4fc418cd1b46642ff2ab863ccfcb9871bd33d252d1abb#0
Enter value for --password-of-payer (the password of the key pair of the payer account): chocolate
Adding transaction 3d50a11b062a49e4be58c8f39c2f5b8d8de0e25ed307a528e9162cba2913b126... 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 9893c8bd5a40c4ae29f4fc418cd1b46642ff2ab863ccfcb9871bd33d252d1abb#0 --uri ws://panarea.hotmoka.io:8001
class io.hotmoka.tutorial.examples.ponzi.GradualPonzi (from jar installed at 09e0121a32bca53d3492834318d68c7dafedf478c8dd92cec549ac3f7ced7295)
  MINIMUM_INVESTMENT:java.math.BigInteger = 1000
  investors:io.takamaka.code.util.StorageList = 9893c8bd5a40c4ae29f4fc418cd1b46642ff2ab863ccfcb9871bd33d252d1abb#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 9893c8bd5a40c4ae29f4fc418cd1b46642ff2ab863ccfcb9871bd33d252d1abb#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 = 9893c8bd5a40c4ae29f4fc418cd1b46642ff2ab863ccfcb9871bd33d252d1abb#2
  last:io.takamaka.code.util.StorageLinkedList$Node = c45bf965d4a90335a633856b959021c8a3608de5265d8e3a7863457383bcec57#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.