Skip to content

serpent Cheat Sheet

Ren Zhang edited this page Jul 7, 2017 · 2 revisions

This is an abridged version of serpent wiki for this tutorial.

Differences Between Serpent and Python

The important differences between Serpent and Python are:

  • Python numbers have potentially unlimited size, Serpent numbers wrap around 2256. For example, in Serpent the expression 3^(2^254) suprisingly evaluates to 1, even though in reality the actual integer is too large to be recorded in its entirety within the universe.
  • Serpent has no decimals.
  • Serpent has no list comprehensions (expressions like [x**2 for x in my_list]), dictionaries or most other advanced features
  • Serpent has no concept of first-class functions. Contracts do have functions, and can call their own functions, but variables (except storage) do not persist across calls.
  • Serpent has a concept of persistent storage variables (see below)
  • Serpent has an extern statement used to call functions from other contracts (see below)

Now, let's write our first contract. Paste the following into a file called mul2.se:

def double(x):
    return(x * 2)

This contract is a simple two lines of code, and defines a function. Functions can be called either by transactions or by other contracts, and are the way that Serpent contracts provide an "interface" to other contracts and to transactions; for example, a contract defining a currency might have functions send(to, value) and check_balance(address).

Additionally, the Pyethereum testing environment that we will be using simply assumes that data input and output are in this format.

Now, let's try actually compiling the code. Type:

$ serpent compile mul2.se
604380600b600039604e567c01000000000000000000000000000000000000000000000000000000006000350463eee9720681141560415760043560405260026040510260605260206060f35b505b6000f3

And there we go, that's the hexadecimal form of the code that you can put into transactions. Or, if you want to see opcodes:

$ serpent pretty_compile mul2.se
[PUSH1, 67, DUP1, PUSH1, 11, PUSH1, 0, CODECOPY, PUSH1, 78, JUMP, PUSH29, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, PUSH1, 0, CALLDATALOAD, DIV, PUSH4, 238, 233, 114, 6, DUP2, EQ, ISZERO, PUSH1, 65, JUMPI, PUSH1, 4, CALLDATALOAD, PUSH1, 64, MSTORE, PUSH1, 2, PUSH1, 64, MLOAD, MUL, PUSH1, 96, MSTORE, PUSH1, 32, PUSH1, 96, RETURN, JUMPDEST, POP, JUMPDEST, PUSH1, 0, RETURN]

Example: Name Registry

Having a multiply-by-two function on the blockchain is kind of boring. So let's do something marginally more interesting: a name registry. The main function will be a register(key, value) operation which checks if a given key was already taken, and if is unoccupied then register it with the desired value and return 1; if the key is already occupied return 0. We will also add a second function to check the value associated with a particular key. Let's try it out:

def register(key, value):
    # Key not yet claimed
    if not self.storage[key]:
        self.storage[key] = value
        return(1)
    else:
        return(0)  # Key already claimed

def ask(key):
    return(self.storage[key])

Here, we see a few parts in action. First, we have the key and value variables that the function takes as arguments. The second line is a comment; it does not get compiled and only serves to remind you what the code does. Then, we have a standard if/else clause, which checks if self.storage[key] is zero (ie. unclaimed), and if it is then it sets self.storage[key] = value and returns 1. Otherwise, it returns zero. self.storage is also a pseudo-array, acting like an array but without any particular memory location.

Including files, and calling other contracts

Once your projects become larger, you will not want to put everything into the same file; things become particularly inconvenient when one piece of code needs to create a contract. Fortunately, the process for splitting code into multiple files is quite simple. Make the following two files:

mul2.se:

def double(x):
    return(x * 2)

returnten.se:

extern mul2.se: [double:[int256]:int256]

MUL2 = create('mul2.se')
def returnten():
    return(MUL2.double(5))

Note that here we introduced several new features. Particularly:

  • The create command to create a contract using code from another file
  • The extern keyword to declare a class of contract for which we know the names of the functions
  • The interface for calling other contracts

create is self-explanatory; it creates a contract and returns the address to the contract.

The way extern works is that you declare a class of contract, in this case mul2, and then list in an array the names of the functions, in this case just double. To generate extern mul2.se: [double:[int256]:int256] use

$ serpent mk_signature mul2.se

From there, given any variable containing an address, you can do x.double(arg1) to call the address stored by that variable. The arguments are the values provided to the function. If you provide too few arguments, the rest are filled to zero, and if you provide too many the extra ones are ignored. Function calling also has some other optional arguments:

  • gas=12414 - call the function with 12414 gas instead of the default (all gas)
  • value=10^19 - send 10^19 wei (10 ether) along with the message
  • data=x, datasz=5 - call the function with 5 values from the array x; note that this replaces other function arguments. data without datasz is illegal
  • outsz=7 - by default, Serpent processes the output of a function by taking the first 32 bytes and returning it as a value. However, if outsz is used as here, the function will instead return an array containing 7 values; if you type y = x.fun(arg1, outsz=7) then you will be able to access the output via y[0], y[1], etc.

To call other contracts on the blockchain, see Examples/3.call_another_contract.py in this repository as a minimum working example.

Storage data structures

In more complicated contracts, you will often want to store data structures in storage to represent certain objects. For example, you might have a decentralized exchange contract that stores the balances of users in multiple currencies, as well as open bid and ask orders where each order has a price and a quantity. For this, Serpent has a built-in mechanism for defining your own structures. For example, in such a decentralized exchange contract you might see:

data user_balances[][]
data orders[](buys[](user, price, quantity), sells[](user, price, quantity))

Then, you might do something like:

def fill_buy_order(currency, order_id):
    # Available amount buyer is willing to buy
    q = self.orders[currency].buys[order_id].quantity
    # My balance in the currency
    bal = self.user_balances[msg.sender][currency]
    # The buyer
    buyer = self.orders[currency].buys[order_id].user
    if q > 0:
        # The amount we can actually trade
        amount = min(q, bal)
        # Trade the currency against the base currency
        self.user_balances[msg.sender][currency] -= amount
        self.user_balances[buyer][currency] += amount
        self.user_balances[msg.sender][0] += amount * self.orders[currency].buys[order_id].price
        self.user_balances[buyer][0] -= amount * self.orders[currency].buys[order_id].price
        # Reduce the remaining quantity on the order
        self.orders[currency].buys[order_id].quantity -= amount

Notice how we define the data structures at the top, and then use them throughout the contract. These data structure gets and sets are converted into storage accesses in the background, so the data structures are persistent.

The language for doing data structures is simple. First, we can do simple variables:

data blah

x = self.blah
self.blah = x + 1

Then, we can do arrays, both finite and infinite:

data blah[1243]
data blaz[]

x = self.blah[505]
y = self.blaz[3**160]
self.blah[125] = x + y

Note that finite arrays are always preferred, because it will cost less gas to calculate the storage index associated with a finite array index lookup. And we can do tuples, where each element of the tuple is itself a valid data structure:

data body(head(eyes[2], nose, mouth), arms[2], legs[2])

x = self.body.head.nose
y = self.body.arms[1]

And we can do arrays of tuples:

data bodies[100](head(eyes[2], nose, mouth), arms[2](fingers[5], elbow), legs[2])

x = self.bodies[45].head.eyes[1]
y = self.bodies[x].arms[1].fingers[3]

Note that the following is unfortunately not legal:

data body(head(eyes[2], nose, mouth), arms[2], legs[2])

x = self.body.head
y = x.eyes[0]

Accesses have to descend fully in a single statement. To see how this could be used in a simpler example, let's go back to our name registry, and upgrade it so that when a user registers a key they become the owner of that key, and the owner of a key has the ability to (1) transfer ownership, and (2) change the value. We'll remove the return values here for simplicity.

data registry[](owner, value)

def register(key):
    # Key not yet claimed
    if not self.registry[key].owner:
        self.registry[key].owner = msg.sender

def transfer_ownership(key, new_owner):
    if self.registry[key].owner == msg.sender:
        self.registry[key].owner = new_owner

def set_value(key, new_value):
    if self.registry[key].owner == msg.sender:
        self.registry[key].value = new_value

def ask(key):
    return([self.registry[key].owner, self.registry[key].value], items=2)

Note that in the last ask command, the function returns an array of 2 values. If you wanted to call the registry, you would have needed to do something like o = registry.ask(key, outsz=2) and you could have then used o[0] and o[1] to recover the owner and value.

Simple arrays in memory

The syntax for arrays in memory are different: they can only be finite and cannot have tuples or more complicated structures.

Example:

def bitwise_or(x, y):
    blah = array(1243)
    blah[567] = x
    blah[568] = y
    blah[569] = blah[567] | blah[568]
    return(blah[569])

There are also two functions for dealing with arrays:

len(x)

Returns the length of array x.

slice(x, items=start, items=end)

Takes a slice of x starting with position start and ending with position end (note that we require end >= start; otherwise the result will almost certainly result in an error)

Arrays and Functions

Functions can also take arrays as arguments, and return arrays.

def compose(inputs:arr):
    return(inputs[0] + inputs[1] * 10 + inputs[2] * 100)

def decompose(x):
    return([x % 10, (x % 100) / 10, x / 100]:arr)

Putting the :arr after a function argument means it is an array, and putting it inside a return statement returns the value as an array (just doing return([x,y,z]) would return the integer which is the memory location of the array).

If a contract calls one of its functions, then it will autodetect which arguments should be arrays and parse them accordingly, so this works fine:

def compose(inputs:arr, radix):
    return(inputs[0] + inputs[1] * radix + inputs[1] * radix ** 2)

def main():
    return self.compose([1,2,3,4,5], 100)

However, if a contract wants to call another contract that takes arrays as arguments, then you will need to put a "signature" into the extern declaration:

extern composer: [compose:[int256[],int256]:int256, main:[]:int256]

If you want to determine the signature to use from a given file, you can do:

> serpent mk_signature compose_test.se
extern compose_test: [compose:[int256[],int256]:int256, main:[]:int256]

Strings

There are two types of strings in Serpent: short strings, eg. "george", and long strings, eg. text("afjqwhruqwhurhqkwrhguqwhrkuqwrkqwhwhrugquwrguwegtwetwet"). Short strings, given simply in quotes as above, are treated as numbers; long strings, surrounded by the text keyword as above, are treated as array-like objects; you can do getch(str, index) and setch(str, index) to manipulate characters in strings (doing str[0] will treat the string as an array and try to fetch the first 32 characters as a number).

To use strings as function arguments or outputs, use the str tag, much like you would use arr for arrays. len(s) gives you the length of a string, and slice works for strings the same way as for arrays too.

Here is an example of returning/retrieving a string:

data str

def t2():
    self.str = text("01")
    log(data=self.str)
    return(self.str, chars=2)

def runThis():
    s = self.t2(outsz=2)
    log(data=s)

Miscellaneous

The three other useful features in the tester environment are:

  • Block access - you can dig around s.block to see block data (eg. s.block.number, s.block.get_balance(addr), s.block.get_storage_data(addr, index))
  • Snapshots - you can do x = s.snapshot() and s.revert(x)
  • Advancing blocks - you can do s.mine(100) and 100 blocks magically pass by with a 60-second interval between blocks. s.mine(100, addr) mines into a particular address.
  • Full block data dump - type s.block.to_dict()

Serpent also gives you access to many "special variables"; the full list is:

  • tx.origin - the sender of the transaction
  • tx.gasprice - gas price of the transaction
  • msg.gas - estimated gas by sender
  • msg.sender - the sender of the message
  • msg.value - the number of wei (smallest units of ether) sent with the message
  • self - the contract's own address
  • self.balance - the contract's balance
  • x.balance (for any x) - that account's balance
  • block.coinbase - current block miner's address
  • block.timestamp - current block timestamp
  • block.prevhash - previous block hash
  • block.difficulty - current block difficulty
  • block.number - current block number
  • block.gaslimit - current block gaslimit

Serpent recognises the following "special functions":

  • def init(): - executed upon contract creation, accepts no parameters
  • def shared(): - executed before running init and user functions
  • def any(): - executed before any user functions

There are also special commands for a few crypto operations; particularly:

  • addr = ecrecover(h, v, r, s) - determines the address that produced the elliptic curve signature v, r, s of the hash h
  • x = sha256(a, items=4) - returns the sha256 hash of the 128 bytes consisting of the 4-item array starting from a
  • x = ripemd160(a, items=4) - same as above but for ripemd160
  • To hash an arbitrary number of bytes, use chars syntax. Example: x = sha256([0xf1fc122bc7f5d74df2b9441a42a1469500000000000000000000000000000000], chars=16) - returns the sha256 of the first 16 bytes. Note: padding with trailing zeroes, otherwise the first 16 bytes will be zeroes, and the sha256 of it will be computed instead of the desired.
  • you can also use sha3 instead of sha256, e.g. sha3(a, items=4)

Tips

  • If a function is not returning the result you expect, double-check that all variables are correct: there is no error/warning when using an undeclared variable.

  • Invalid argument count or LLL function usually means you just called foo() instead of self.foo().

  • Sometimes you may be intending to use unsigned operators. eg div() and lt() instead of '/' and '<'.

  • Be careful that if your flow requires going through a number of contracts, that someone can't just directly short-circuit and call one of your latter contracts with data they've manipulated elsewhere. Example: If you have contract C which gives someone ether, but relies on computation from Contract A->B->C, that someone can't just call B or C to give themselves ether.