Inspecting Tezos smart contracts with PyTezos library

How to convert Tezos smart contract data between different formats, and how this can help to understand the idea behind the code.

Michael
Michael   Follow

In this post I will show you how to convert Tezos smart contract data between different formats, and how this can help to understand the idea behind the code.

# Introduction

Blockchain data availability is a bit of a controversial thing: theoretically you have access to everything, but when it comes to actions, you realize that things aren’t that simple. In fact, you need to have enough resources, a bunch of middleware, and a particular tech stack knowledge for direct access to the data. Therefore, blockchain explorers play a huge role for the data accessibility, they drastically simplify information retrieving and analyzing.

But that’s not all, proper data representation plays an equally important role, cause humans require much more higher abstractions, visualizations, and main components extraction.

We come to the topic of this post — smart contracts. It’s crucial to understand what is happening inside the smart contract you trust your funds to. Tezos went further than projects that use bytecode virtual machines, and introduced Michelson — a language designed as a readable compilation target. But it’s still not easy to read it because you need to think like a stack machine and transform nested structures on the fly.

I would like to show how simple conversions can improve the perception and help you understand the purpose of a smart contract in general.

# Notebook

# Run locally

You will need python 3.6+ and Jupyter package to continue.

$ git clone https://github.com/baking-bad/pytezos.git$ cd pytezos$ jupyter notebook

Navigate to the `examples` folder and run the dyor_smart_contracts.ipynb.

# Play on Google Colab

You can also play around with the code online: https://colab.research.google.com/drive/1DICb442LL2hARTxUgV91g_sWURDFwGzK

# Searching for origination

As was shown in the previous post, thanks to Tezos statefulness we can rather quickly look for transactions of a special kind.
First af all let's try to find a smart contract origination.

from pytezos.rpc import alphanet, mainnet
from pytezos.rpc.search import SearchChain
stderr
sc = SearchChain.from_chain(mainnet.main)

We will look for one of the TezVote smart contracts.
The algorithm checks for the contract existence, before the origination RPC query have to return 404.

origination = sc.find_contract_origination_operation('KT1ExvG3EjTrvDcAU7EqLNb77agPa5u6KvnY')
stderr
2019-03-24 01:57:19.634 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 182605 2019-03-24 01:57:20.210 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 273907 2019-03-24 01:57:20.801 | DEBUG | pytezos.rpc.search:bisect:31 - 0 at level 319558 2019-03-24 01:57:21.375 | DEBUG | pytezos.rpc.search:bisect:31 - 0 at level 296732 2019-03-24 01:57:21.967 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 285319 2019-03-24 01:57:22.579 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 291025 2019-03-24 01:57:23.172 | DEBUG | pytezos.rpc.search:bisect:31 - 0 at level 293878 2019-03-24 01:57:23.744 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 292451 2019-03-24 01:57:24.539 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 293164 2019-03-24 01:57:25.306 | DEBUG | pytezos.rpc.search:bisect:31 - 0 at level 293521 2019-03-24 01:57:26.076 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 293342 2019-03-24 01:57:26.866 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 293431 2019-03-24 01:57:27.650 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 293476 2019-03-24 01:57:28.412 | DEBUG | pytezos.rpc.search:bisect:31 - 0 at level 293498 2019-03-24 01:57:29.209 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 293487 2019-03-24 01:57:29.983 | DEBUG | pytezos.rpc.search:bisect:31 - 0 at level 293492 2019-03-24 01:57:30.776 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 293489 2019-03-24 01:57:31.557 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 293490 2019-03-24 01:57:32.332 | DEBUG | pytezos.rpc.search:bisect:31 - None at level 293491
origination()
result
{'protocol': 'PsddFKi32cMJ2qPjf43Qv5GDWLDPZb3T3bF6fLKiF5HtvHNU7aP', 'chain_id': 'NetXdQprcVkpaWU', 'hash': 'ooYyqjxeAcfgQDUfQPZ7rgrdqkGN61J1twQ3TWth3YnTPPgKuii', 'branch': 'BM7oCo9haDP4tRxUDECZTWvE1joLoP1xTV5EJSb53PdDkksFokJ', 'contents': \[{'kind': 'reveal', 'source': 'tz1MXrEgDNnR8PDryN8sq4B2m9Pqcf57wBqM', 'fee': '1259', 'counter': '983246', 'gas_limit': '10000', 'storage_limit': '0', 'public_key': 'edpktkZZBRwQk6BKTirT8tP2o2rFivY2cQ3vcZevnzg3SaCZLRqiL1', 'metadata': {'balance_updates': \[{'kind': 'contract', 'contract': 'tz1MXrEgDNnR8PDryN8sq4B2m9Pqcf57wBqM', 'change': '-1259'}, {'kind': 'freezer', 'category': 'fees', 'delegate': 'tz1Ldzz6k1BHdhuKvAtMRX7h5kJSMHESMHLC', 'level': 71, 'change': '1259'}\], 'operation_result': {'status': 'applied', 'consumed_gas': '10000'}}}, {'kind': 'origination', 'source': 'tz1MXrEgDNnR8PDryN8sq4B2m9Pqcf57wBqM', 'fee': '1559', 'counter': '983247', 'gas_limit': '12997', 'storage_limit': '377', 'managerPubkey': 'tz1MXrEgDNnR8PDryN8sq4B2m9Pqcf57wBqM', 'balance': '0', 'spendable': False, 'delegatable': False, 'script': {'code': \[{'prim': 'parameter', 'args': \[{'prim': 'nat'}\]}, {'prim': 'storage', 'args': \[{'prim': 'map', 'args': \[{'prim': 'address'}, {'prim': 'nat'}\]}\]}, {'prim': 'code', 'args': \[\[{'prim': 'DUP'}, {'prim': 'CDR'}, {'prim': 'SWAP'}, {'prim': 'CAR'}, {'prim': 'DUP'}, {'prim': 'PUSH', 'args': \[{'prim': 'nat'}, {'int': '2'}\]}, \[\[{'prim': 'COMPARE'}, {'prim': 'GE'}\], {'prim': 'IF', 'args': \[\[\], \[\[{'prim': 'UNIT'}, {'prim': 'FAILWITH'}\]\]\]}\], {'prim': 'SOME'}, {'prim': 'SENDER'}, {'prim': 'UPDATE'}, {'prim': 'NIL', 'args': \[{'prim': 'operation'}\]}, {'prim': 'PAIR'}\]\]}\], 'storage': \[\]}, 'metadata': {'balance_updates': \[{'kind': 'contract', 'contract': 'tz1MXrEgDNnR8PDryN8sq4B2m9Pqcf57wBqM', 'change': '-1559'}, {'kind': 'freezer', 'category': 'fees', 'delegate': 'tz1Ldzz6k1BHdhuKvAtMRX7h5kJSMHESMHLC', 'level': 71, 'change': '1559'}\], 'operation_result': {'status': 'applied', 'balance_updates': \[{'kind': 'contract', 'contract': 'tz1MXrEgDNnR8PDryN8sq4B2m9Pqcf57wBqM', 'change': '-100000'}, {'kind': 'contract', 'contract': 'tz1MXrEgDNnR8PDryN8sq4B2m9Pqcf57wBqM', 'change': '-257000'}\], 'originated_contracts': \['KT1ExvG3EjTrvDcAU7EqLNb77agPa5u6KvnY'\], 'consumed_gas': '12897', 'storage_size': '100', 'paid_storage_size_diff': '100'}}}\], 'signature': 'sigbbmyQB1CaZsn91jZeGsUt5io7uxz5n5epnFiix1ef1evt17NHYwzuRxpKnUZhd7efFyHY14wkyfn7mMCxyM3FMKGXVd8Z'}

# Finding all storage altering transactions

We can also use step&bisect algo to retrieve all transactions that have changed the SC storage.

for op in sc.find_storage_change_operations('KT1ExvG3EjTrvDcAU7EqLNb77agPa5u6KvnY', origination_level=293492):
    break
stderr
2019-03-24 01:57:36.053 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 364490 2019-03-24 01:57:36.640 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 363770 2019-03-24 01:57:37.423 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 363050 2019-03-24 01:57:38.033 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 362330 2019-03-24 01:57:38.614 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 361610 2019-03-24 01:57:39.204 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 360890 2019-03-24 01:57:39.777 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 360170 2019-03-24 01:57:40.363 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 359450 2019-03-24 01:57:40.954 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 358730 2019-03-24 01:57:41.710 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 358010 2019-03-24 01:57:42.495 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 357290 2019-03-24 01:57:43.289 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 356570 2019-03-24 01:57:44.091 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 355850 2019-03-24 01:57:44.907 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 355130 2019-03-24 01:57:45.698 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 354410 2019-03-24 01:57:46.471 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 353690 2019-03-24 01:57:47.259 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 352970 2019-03-24 01:57:48.041 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 352250 2019-03-24 01:57:48.827 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 351530 2019-03-24 01:57:49.591 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 350810 2019-03-24 01:57:50.378 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 350090 2019-03-24 01:57:51.164 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 349370 2019-03-24 01:57:51.963 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 348650 2019-03-24 01:57:52.771 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 347930 2019-03-24 01:57:53.559 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 347210 2019-03-24 01:57:54.319 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 346490 2019-03-24 01:57:54.909 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 345770 2019-03-24 01:57:55.505 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 345050 2019-03-24 01:57:56.099 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 344330 2019-03-24 01:57:56.691 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 343610 2019-03-24 01:57:57.276 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 342890 2019-03-24 01:57:57.858 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 342170 2019-03-24 01:57:58.474 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 341450 2019-03-24 01:57:59.063 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 340730 2019-03-24 01:57:59.654 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 340010 2019-03-24 01:58:00.235 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 339290 2019-03-24 01:58:00.842 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 338570 2019-03-24 01:58:01.447 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 337850 2019-03-24 01:58:02.059 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 337130 2019-03-24 01:58:02.652 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 336410 2019-03-24 01:58:03.235 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 335690 2019-03-24 01:58:03.825 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - 8212527456151390696 at level 334970 2019-03-24 01:58:04.420 | DEBUG | pytezos.rpc.search:find_state_change_intervals:16 - -5983102954595768091 at level 334250 2019-03-24 01:58:05.029 | DEBUG | pytezos.rpc.search:bisect:31 - 8212527456151390696 at level 334610 2019-03-24 01:58:05.621 | DEBUG | pytezos.rpc.search:bisect:31 - 8212527456151390696 at level 334430 2019-03-24 01:58:06.229 | DEBUG | pytezos.rpc.search:bisect:31 - 8212527456151390696 at level 334340 2019-03-24 01:58:06.805 | DEBUG | pytezos.rpc.search:bisect:31 - 8212527456151390696 at level 334295 2019-03-24 01:58:07.404 | DEBUG | pytezos.rpc.search:bisect:31 - -5983102954595768091 at level 334272 2019-03-24 01:58:07.984 | DEBUG | pytezos.rpc.search:bisect:31 - -5983102954595768091 at level 334283 2019-03-24 01:58:08.598 | DEBUG | pytezos.rpc.search:bisect:31 - 8212527456151390696 at level 334289 2019-03-24 01:58:09.201 | DEBUG | pytezos.rpc.search:bisect:31 - -5983102954595768091 at level 334286 2019-03-24 01:58:09.787 | DEBUG | pytezos.rpc.search:bisect:31 - -5983102954595768091 at level 334287 2019-03-24 01:58:10.383 | DEBUG | pytezos.rpc.search:bisect:31 - 8212527456151390696 at level 334288
op()
result
{'protocol': 'PsddFKi32cMJ2qPjf43Qv5GDWLDPZb3T3bF6fLKiF5HtvHNU7aP', 'chain_id': 'NetXdQprcVkpaWU', 'hash': 'ooJo7FLs23FFV6Nh7T9JePESirCSaQDmYKsjwvpWkGxaXdrwo88', 'branch': 'BKs7j5NTxaMoqA6oR5z2tGrrGHtVLLGKPamdEkVBt9Ayuh3Brjr', 'contents': \[{'kind': 'transaction', 'source': 'tz1hcHHoeKQCqsXDTmE2WS1B5FGGsbJVsbp7', 'fee': '21000', 'counter': '875786', 'gas_limit': '32811', 'storage_limit': '50', 'amount': '0', 'destination': 'KT1ExvG3EjTrvDcAU7EqLNb77agPa5u6KvnY', 'parameters': {'int': '2'}, 'metadata': {'balance_updates': \[{'kind': 'contract', 'contract': 'tz1hcHHoeKQCqsXDTmE2WS1B5FGGsbJVsbp7', 'change': '-21000'}, {'kind': 'freezer', 'category': 'fees', 'delegate': 'tz1TDSmoZXwVevLTEvKCTHWpomG76oC9S2fJ', 'level': 81, 'change': '21000'}\], 'operation_result': {'status': 'applied', 'storage': \[{'prim': 'Elt', 'args': \[{'bytes': '00000a806e3d01d6178278c8120f861126ac5d4e70dd'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '000014c507140f5f3c75052e12b1975e3adfda9ad8d8'}, {'int': '1'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '00002accc28c028ee4e68e990d47baa9769cc0f776ea'}, {'int': '1'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '00002eeb56bdbbc7ea0b8ce4b8f09c1fec344bcd1151'}, {'int': '1'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '00006f256ddef23b26433f970e856c1f9aea1134cee4'}, {'int': '1'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '00007173e7d3b20f38a430ef3c2680002f031a3eafc4'}, {'int': '1'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '00008fb5cea62d147c696afd9a93dbce962f4c8a9c91'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '000092e4e012a95c52c068d892fbc6b619e17a36c557'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '0000b71bb0a81e36ebe7a8b256006081b593991265ec'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '0000d9a0c60e0b5718b8bab3bf611c0a4fbbab4c1dc4'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '0000f0fe1f03312284630df8f6c3a8ed4ea0c5119f4a'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '0000fed86ac2172e3841bb3da1ee969c4e619163a2ae'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '00016e19cd2d628167c47f74b60c84c01c67b2d7a49a'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '010b32e023ce9c4d2294b215daa1e43561e611aec500'}, {'int': '1'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '01704dc19cc2d13d03183557905ce5a362a09f035700'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '0170b2066acd8ce308e94efc3fd9764919f95098c900'}, {'int': '1'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '01ab062220312a77f03738c1b2aad1334276c7bfae00'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '01c8c724b479e44f5bf1fe5251cd6c0f09879c675300'}, {'int': '1'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '01e360a38d47a089dc1da835ce3b2d740483f499cf00'}, {'int': '1'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '01e8d513978c641c795c621a83d2c10a4daaeb397100'}, {'int': '2'}\]}, {'prim': 'Elt', 'args': \[{'bytes': '01f0fe7d8f70bc9683b92fd17df37346fee4eb142400'}, {'int': '1'}\]}\], 'balance_updates': \[{'kind': 'contract', 'contract': 'tz1hcHHoeKQCqsXDTmE2WS1B5FGGsbJVsbp7', 'change': '-31000'}\], 'consumed_gas': '32711', 'storage_size': '751', 'paid_storage_size_diff': '31'}}}\], 'signature': 'sigj7qDjuneTzTUzL82ggkr6WzQvpW2kMQtKBPWmDe8iFLJaVtv3AQuDDNmMXBTWPUWMqoC34WqEG72tbCZYKyXUNvHSqzEy'}

# Decoding storage

Allright, now let's view some data, i'll take some random SC from alphanet for demonstration

alphanet.context.contracts['KT19iGCL4YrVpT6ezEzbDH37Yxbas8jWQz4s'].storage()
result
{'prim': 'Pair', 'args': \[\[{'prim': 'Elt', 'args': \[{'string': 'tz1PKKC9NTsxxfwYpg1bVAY5k5AKuPYe9gpg'}, {'string': 'KT1BmGdHyTXaxKzEY55xmQpPspEBvQcgnrKn'}\]}\], {'prim': 'Pair', 'args': \[\[{'prim': 'Elt', 'args': \[{'string': 'tz1PKKC9NTsxxfwYpg1bVAY5k5AKuPYe9gpg'}, {'string': 'KT1BmGdHyTXaxKzEY55xmQpPspEBvQcgnrKn'}\]}\], \[{'prim': 'Elt', 'args': \[{'string': 'KT1BmGdHyTXaxKzEY55xmQpPspEBvQcgnrKn'}, {'int': '1'}\]}\]\]}\]}
alphanet.context.contracts['KT1VnRY6UuWH89P8DQtC7Sd178jkckff8o8d'].storage()
result
{'prim': 'Pair', 'args': \[\[\], {'prim': 'Pair', 'args': \[{'int': '1'}, {'prim': 'Pair', 'args': \[{'int': '111111111111111110'}, {'prim': 'Pair', 'args': \[{'int': '18'}, {'prim': 'Pair', 'args': \[{'string': 'test1'}, {'prim': 'Pair', 'args': \[{'string': 'TST'}, {'string': 'KT1GE2AZhazRxGsAjRVkQccHcB2pvANXQWd7'}\]}\]}\]}\]}\]}\]}
alphanet.context.contracts['KT1FEDVALSfQLZwVZbF1hRxJ9c8MTPe7azCZ'].storage()
result
{'prim': 'Pair', 'args': \[\[{'prim': 'Elt', 'args': \[{'int': '0'}, {'prim': 'Pair', 'args': \[{'string': ''}, {'prim': 'Pair', 'args': \[{'string': ''}, {'prim': 'Pair', 'args': \[{'int': '0'}, {'prim': 'Pair', 'args': \[{'string': 'tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG'}, {'prim': 'Pair', 'args': \[{'string': 'tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG'}, {'prim': 'Pair', 'args': \[{'prim': 'Pair', 'args': \[{'prim': 'True'}, {'prim': 'Pair', 'args': \[{'string': ''}, {'string': ''}\]}\]}, {'prim': 'Pair', 'args': \[{'string': '1970-01-01T00:00:00Z'}, {'string': '1970-01-01T00:00:00Z'}\]}\]}\]}\]}\]}\]}\]}\]}, {'prim': 'Elt', 'args': \[{'int': '1'}, {'prim': 'Pair', 'args': \[{'string': '2035 Moon Lander'}, {'prim': 'Pair', 'args': \[{'string': 'A shuttle like no other. Experiance a ride to the moon, retro style.'}, {'prim': 'Pair', 'args': \[{'int': '1000000'}, {'prim': 'Pair', 'args': \[{'string': 'tz1KwNFuzryhY7ZFyZVcdnHFEZCT9xgrRmWT'}, {'prim': 'Pair', 'args': \[{'string': 'tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG'}, {'prim': 'Pair', 'args': \[{'prim': 'Pair', 'args': \[{'prim': 'False'}, {'prim': 'Pair', 'args': \[{'string': 'Moon Lander was damaged.'}, {'string': 'nil'}\]}\]}, {'prim': 'Pair', 'args': \[{'string': '2019-04-20T04:00:00Z'}, {'string': '2019-05-20T04:00:00Z'}\]}\]}\]}\]}\]}\]}\]}\]}\], {'int': '1'}\]}

Okay that won't work, let's do some magic:

alphanet.context.contracts['KT19iGCL4YrVpT6ezEzbDH37Yxbas8jWQz4s'].decode_storage()
result
{'owner_to_proxy': {'tz1PKKC9NTsxxfwYpg1bVAY5k5AKuPYe9gpg': 'KT1BmGdHyTXaxKzEY55xmQpPspEBvQcgnrKn'}, 'recovery_to_proxy': {'tz1PKKC9NTsxxfwYpg1bVAY5k5AKuPYe9gpg': 'KT1BmGdHyTXaxKzEY55xmQpPspEBvQcgnrKn'}, 'proxy_to_nonce': {'KT1BmGdHyTXaxKzEY55xmQpPspEBvQcgnrKn': 1}}
alphanet.context.contracts['KT1VnRY6UuWH89P8DQtC7Sd178jkckff8o8d'].decode_storage()
result
{'accounts': {}, 'version': 1, 'totalSupply': 111111111111111110, 'decimals': 18, 'name': 'test1', 'symbol': 'TST', 'owner': 'KT1GE2AZhazRxGsAjRVkQccHcB2pvANXQWd7'}
alphanet.context.contracts['KT1FEDVALSfQLZwVZbF1hRxJ9c8MTPe7azCZ'].decode_storage()
result
({0: ('', '', Decimal('0'), 'tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG', 'tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG', True, '', '', DateTime(1970, 1, 1, 0, 0, 0, tzinfo=Timezone('+00:00')), DateTime(1970, 1, 1, 0, 0, 0, tzinfo=Timezone('+00:00'))), 1: ('2035 Moon Lander', 'A shuttle like no other. Experiance a ride to the moon, retro style.', Decimal('1'), 'tz1KwNFuzryhY7ZFyZVcdnHFEZCT9xgrRmWT', 'tz1gH29qAVaNfv7imhPthCwpUBcqmMdLWxPG', False, 'Moon Lander was damaged.', 'nil', DateTime(2019, 4, 20, 4, 0, 0, tzinfo=Timezone('+00:00')), DateTime(2019, 5, 20, 4, 0, 0, tzinfo=Timezone('+00:00')))}, 1)

Oh yeah, much better 😃
Note that we can handle both annotated and not annotated storage.

# Encoding storage

We can easily encode storage data back, this functional is quite useful for unit testing.

contract = alphanet.context.contracts['KT1VnRY6UuWH89P8DQtC7Sd178jkckff8o8d']

Let's look at the schema first

contract.storage_schema()
result
{'accounts': {'#address': {'balance': '#nat', 'allowances': {'#address': '#nat'}}}, 'version': '#nat', 'totalSupply': '#nat', 'decimals': '#nat', 'name': '#string', 'symbol': '#string', 'owner': '#address'}
contract.encode_storage({
    'accounts': {},
    'version': 2,
    'totalSupply': 100000000,
    'decimals': 8,
    'name': 'AbcCoin',
    'symbol': 'ABC',
    'owner': 'KT1GE2AZhazRxGsAjRVkQccHcB2pvANXQWd7'
})
result
{'prim': 'Pair', 'args': \[\[\], {'prim': 'Pair', 'args': \[{'int': '2'}, {'prim': 'Pair', 'args': \[{'int': '100000000'}, {'prim': 'Pair', 'args': \[{'int': '8'}, {'prim': 'Pair', 'args': \[{'string': 'AbcCoin'}, {'prim': 'Pair', 'args': \[{'string': 'ABC'}, {'string': 'KT1GE2AZhazRxGsAjRVkQccHcB2pvANXQWd7'}\]}\]}\]}\]}\]}\]}

# Decoding transaction parameters

Great! But what about parameters we call smart contracts with?
Take some complicated example

content = alphanet.blocks[216905].operations.managers.contents()[0]
contract = alphanet.context.contracts['KT1FU74GimCeEVRAEZGURb6TWU8jK1N6zFJy']
content['parameters']
result
{'prim': 'Left', 'args': \[{'prim': 'Pair', 'args': \[{'int': '200'}, {'prim': 'Pair', 'args': \[{'bytes': '8262f06395dbe509f27c1860774637394783df273e7f94ceac5914e2bcbb5faa'}, {'prim': 'Pair', 'args': \[{'string': 'tz1ZvVYfqHUzmXNekaZRRGaFucH2DD6Q88Yc'}, {'prim': 'True'}\]}\]}\]}\]}
contract.decode_parameters(content['parameters'])
result
{'Initiate': {'iRefTime': 200, 'iHSec': '8262f06395dbe509f27c1860774637394783df273e7f94ceac5914e2bcbb5faa', 'iPartie': 'tz1ZvVYfqHUzmXNekaZRRGaFucH2DD6Q88Yc', 'iMaster': True}}

As you can see, we combine type and field annotations from the code with the transaction data.

# Encoding transaction parameters

If we do not know exactly the SC interface, we can look at the internal schema representation:

contract.parameter_schema()
result
{'Initiate': {'iRefTime': '#nat', 'iHSec': '#bytes', 'iPartie': '#address', 'iMaster': '#bool'}, 'Redeem': {'rdHSec': '#bytes', 'rdSec': '#bytes'}, 'Refund': '#bytes'}
contract.encode_parameters({'Redeem': {'rdHSec': '12', 'rdSec': '34'}})
result
{'prim': 'Right', 'args': \[{'prim': 'Left', 'args': \[{'prim': 'Pair', 'args': \[{'bytes': '12'}, {'bytes': '34'}\]}\]}\]}

# Visualize storage diff

storage_1 = mainnet.blocks[328451].context.contracts['KT1ExvG3EjTrvDcAU7EqLNb77agPa5u6KvnY'].decode_storage()
storage_1
result
{'tz1MXrEgDNnR8PDryN8sq4B2m9Pqcf57wBqM': 1, 'tz1Pv7ha8wzAyRHkR9RahWapdopK1tt6nVz1': 1, 'tz1Vyuu4EJ5Nym4JcrfRLnp3hpaq1DSEp1Ke': 1, 'tz1Z2jXfEXL7dXhs6bsLmyLFLfmAkXBzA9WE': 2, 'tz1cLDXASgh48ntYmLqM3cqPEXmUtpJVVPma': 2, 'tz1fUjvVhJrHLZCbhPNvDRckxApqbkievJHN': 2, 'tz1isXamBXpTUgbByQ6gXgZQg4GWNW7r6rKE': 2, 'KT19bywme8Dvown1K4VvbYfBtRZr967VTw93': 1, 'KT1JreWebFqXAYLsd7M4PZat8aKMdRQtW9jb': 1, 'KT1StPGrbvca76TrwBoJrDZrfTqireXRiKcP': 1, 'KT1VK2iu1sMRWZRDrtRVF7rUSRxEfkpRx2Dj': 1, 'KT1Vosb5zuhciCCzYxLggdLLFGXKaT47UFwm': 2, 'KT1WZ2c7UsvUtrpd9ZZ7En266Cft7Ztdh4N3': 1}
storage_2 = mainnet.blocks[334288].context.contracts['KT1ExvG3EjTrvDcAU7EqLNb77agPa5u6KvnY'].decode_storage()
storage_2
result
{'tz1LbZJZ3ZiNCebiDQghuJCyByCmnsvG1RhV': 2, 'tz1MXrEgDNnR8PDryN8sq4B2m9Pqcf57wBqM': 1, 'tz1PYLN9TsKZHfn2GtrXnxkeGvahmYdBTG5v': 1, 'tz1Pv7ha8wzAyRHkR9RahWapdopK1tt6nVz1': 1, 'tz1VmiY38m3y95HqQLjMwqnMS7sdMfGomzKi': 1, 'tz1Vyuu4EJ5Nym4JcrfRLnp3hpaq1DSEp1Ke': 1, 'tz1Yju7jmmsaUiG9qQLoYv35v5pHgnWoLWbt': 2, 'tz1Z2jXfEXL7dXhs6bsLmyLFLfmAkXBzA9WE': 2, 'tz1cLDXASgh48ntYmLqM3cqPEXmUtpJVVPma': 2, 'tz1fUjvVhJrHLZCbhPNvDRckxApqbkievJHN': 2, 'tz1hcHHoeKQCqsXDTmE2WS1B5FGGsbJVsbp7': 2, 'tz1isXamBXpTUgbByQ6gXgZQg4GWNW7r6rKE': 2, 'tz2JMPu9yVKuX2Au8UUbp7YrKBZJSdYhgwwu': 2, 'KT19bywme8Dvown1K4VvbYfBtRZr967VTw93': 1, 'KT1JpaPrQq7dmFKMhe5S8yrGaJ9pDf8PK8E5': 2, 'KT1JreWebFqXAYLsd7M4PZat8aKMdRQtW9jb': 1, 'KT1QB4Tib11b8gYrC77Xs9bXU8TGJTXPAK7J': 2, 'KT1StPGrbvca76TrwBoJrDZrfTqireXRiKcP': 1, 'KT1VK2iu1sMRWZRDrtRVF7rUSRxEfkpRx2Dj': 1, 'KT1Vosb5zuhciCCzYxLggdLLFGXKaT47UFwm': 2, 'KT1WZ2c7UsvUtrpd9ZZ7En266Cft7Ztdh4N3': 1}
from pytezos.tools.diff import generate_jsondiff_html
generate_jsondiff_html(storage_1, storage_2, output_path='storage_diff.html')

view diff

This is pretty what we need when analyzing a smart contract behaviour, an input and side effects.

# Accessing BigMap

contract.big_map_get('52c5bcbf9cb4dcaacd8689b42726c6f11e6eb575ade913923b6b1420b5b65eb9')
result
{'initTs': DateTime(2019, 3, 5, 11, 33, 18, tzinfo=Timezone('UTC')), 'refTime': 200, 'hSec': '52c5bcbf9cb4dcaacd8689b42726c6f11e6eb575ade913923b6b1420b5b65eb9', 'sec': '65a032b509811aba58cf788a90a00b24da6886a12008bfa9b13264478eaa42fe', 'initor': 'tz1hqnW3wL3AShSAqnHXCJgt25DkEqn7dCqJ', 'partie': 'tz1ZvVYfqHUzmXNekaZRRGaFucH2DD6Q88Yc', 'value': Decimal('10'), 'empt': True, 'state': {'Empty': 'Unit'}}

We can also decode big map delta from the transaction

big_map_diff = alphanet.blocks[216895].operations.contents('transaction')[0]['metadata']['operation_result']['big_map_diff']
big_map_diff
result
\[{'key_hash': 'exprv2HiYAKq5BMmad58uDg7DZTavYaRTfEUERXSW9wqun1UgjjgH9', 'key': {'bytes': 'd8d9564ad3a56df8b386f00368f43760b3a27893f27fa8cc840af02d751794e3'}, 'value': {'prim': 'Pair', 'args': \[{'int': '1551790128'}, {'prim': 'Pair', 'args': \[{'int': '200'}, {'prim': 'Pair', 'args': \[{'bytes': 'd8d9564ad3a56df8b386f00368f43760b3a27893f27fa8cc840af02d751794e3'}, {'prim': 'Pair', 'args': \[{'prim': 'Some', 'args': \[{'bytes': 'cd45c8d595d6926d339a46df7f69256e13c7f1eb7eef13962ed743029dde726c'}\]}, {'prim': 'Pair', 'args': \[{'bytes': '0000f38be9b2a1bfb880fb6bf718924ad3275e4be45e'}, {'prim': 'Pair', 'args': \[{'bytes': '00009caed54e6489d2efc325d276166bd3a05152782d'}, {'prim': 'Pair', 'args': \[{'int': '10000000'}, {'prim': 'Pair', 'args': \[{'prim': 'True'}, {'prim': 'Left', 'args': \[{'prim': 'Unit'}\]}\]}\]}\]}\]}\]}\]}\]}\]}}\]
contract.big_map_diff_decode(big_map_diff)
result
{'d8d9564ad3a56df8b386f00368f43760b3a27893f27fa8cc840af02d751794e3': {'initTs': DateTime(2019, 3, 5, 12, 48, 48, tzinfo=Timezone('UTC')), 'refTime': 200, 'hSec': 'd8d9564ad3a56df8b386f00368f43760b3a27893f27fa8cc840af02d751794e3', 'sec': 'cd45c8d595d6926d339a46df7f69256e13c7f1eb7eef13962ed743029dde726c', 'initor': 'tz1hqnW3wL3AShSAqnHXCJgt25DkEqn7dCqJ', 'partie': 'tz1ZvVYfqHUzmXNekaZRRGaFucH2DD6Q88Yc', 'value': Decimal('10'), 'empt': True, 'state': {'Empty': 'Unit'}}}

# Bonus: parsing .tz files

You can also initialize a Contract instance from Micheline, Michelson source string or file.

source = '''
parameter nat;
storage (map address nat);
code { DUP ;
       CDR ;
       SWAP ;
       CAR ;
       DUP ;
       PUSH nat 2 ;
       { { COMPARE ; GE } ; IF {} { { UNIT ; FAILWITH } } } ;
       SOME ;
       SENDER ;
       UPDATE ;
       NIL operation ;
       PAIR }
'''
from pytezos.rpc.contract import Contract
contract = Contract.from_string(source)
contract
result
\[{'prim': 'parameter', 'args': \[{'prim': 'nat'}\]}, {'prim': 'storage', 'args': \[{'prim': 'map', 'args': \[{'prim': 'address'}, {'prim': 'nat'}\]}\]}, {'prim': 'code', 'args': \[\[{'prim': 'DUP'}, {'prim': 'CDR'}, {'prim': 'SWAP'}, {'prim': 'CAR'}, {'prim': 'DUP'}, {'prim': 'PUSH', 'args': \[{'prim': 'nat'}, {'int': '2'}\]}, \[\[{'prim': 'COMPARE'}, {'prim': 'GE'}\], {'prim': 'IF', 'args': \[\[\], \[\[{'prim': 'UNIT'}, {'prim': 'FAILWITH'}\]\]\]}\], {'prim': 'SOME'}, {'prim': 'SENDER'}, {'prim': 'UPDATE'}, {'prim': 'NIL', 'args': \[{'prim': 'operation'}\]}, {'prim': 'PAIR'}\]\]}\]

# Summary

We have shown that rather simple transformations and metadata usage can drastically increase readability of Tezos smart contract storage (including big maps) and transaction parameters. And we can use inverse transformations to communicate with a smart contract in a more convenient language. We can also make a visualization of storage differences for a specific operation or period.

All this give us the toolkit for black box analysis, which can help to understand the general idea of the code by matching inputs and resulting side effects.

# What’s next

As you may have noticed, we have almost everything to build a unit test platform that depends only on the PyTezos library and public RPC API. While most developers prefer higher abstraction languages, I guess this framework can find its niche. Combined with the excellent PyCharm extension for Michelson, this is a good development environment, what do you think?

Follow us on Twitter for updates and ask anything in our Telegram chat!