# Chapter 2

Working with Tezos RPC: networks, chains, blocks, operations, and context.

In this chapter we will explore main blockchain structures and how to query them using PyTezos.
Tezos node software is built in a modular way, there is a generic Shell which is responsible for basic block validation, p2p, storage, and Protocol that contains main logic. Thanks to the self-amendment feature this protocol code can be changed up to a complete replacement. RPC interface provided by the Tezos node reflects this architecture:

  • There are shell-level endpoints that don't depend on a particular protocol;
  • There are block-level endpoints which can response differently depending on the protocol version (block level).

PyTezos wrapper reflects the RPC endpoint scheme and provides so called "chaining" interface for creating requests. For instance if you want to query GET /a/b/c/d it would be a.b.c.d(). If it's a POST request it would be a.b.c.d.post(). RPC documentation is integrated to the query engine and you can check available methods on each step, just execute e.g. a.b.c in a notebook cell.

# Network

Before moving on to playing with RPC we need to connect to one of Tezos networks. Aside from the mainnet which carries value, there are several public testnets, plus anyone can spawn his own sandboxnet e.g. for debugging dapps.

First of all we create a PyTezos client instance and initialize the network (by default it uses the network with latest known protocol activated):

from pytezos import pytezos

pytezos = pytezos.using(shell='mainnet')

It can also be node uri:

pytezos = pytezos.using(shell='https://mainnet-tezos.giganode.io/')

Note that using method returns a new instance of PyTezos client and does not modifies the existing one.

# Chain

There can be multiple chains at a particular moment in a single network:

  • There is a special testing period in the voting process during which new protocol is battle tested but detached from the real funds.
  • Due to network delays or other reasons two or more block producers can simultaneously extend main chain essentially creating forks. With time (usually 1-2 blocks) all such chains except one with the highest "fitness" become orphans (not adopted by others) and then obsolete.

Keep in mind, that your RPC provider can be at an orphan state at some point of time and rollback only after several minutes.

Most time you will work with the main chain:

<pytezos.rpc.shell.ChainQuery object at 0x7fcf48d768d0>

Properties .path # /chains/main .node # https://mainnet-tezos.giganode.io/ ()

RPC endpoints .blocks .chain_id .checkpoint .invalid_blocks .mempool

Helpers .watermark()

Note the output — this is the builtin documentation mentioned earlier. In particular list of RPC methods is useful for exploration or if you forget something.

# Block

Blocks can be accessed by hash, level (height), special identifiers (head, genesis), or offsets (head-123, head+123).
In a nutshell, block consists of a header and a list of operations. Block hash (blake2b) is actually derived from the byte representation of its header.


There are useful shortcuts for most common scenarios, e.g. accessing head.

{'protocol': 'PsCARTHAGazKbHtnKfLzQg3kms52kSRpgnDY982a9oYsSXRLQEb',
 'chain_id': 'NetXdQprcVkpaWU',
 'hash': 'BLP93MhbrKvMKLsBuy8UErpwn1QYVCR7DHwhD3geWfXgemZY3jm',
 'level': 1131056,
 'proto': 6,
 'predecessor': 'BKkHEv6Qq4cWo14QkfQSNFpWiczCxtZvBGUgmRrD62DEf8YbHwj',
 'timestamp': '2020-09-16T15:51:58Z',
 'validation_pass': 4,
 'operations_hash': 'LLobEPiM5zFMSwFQVMxttzS9sTtBJPhqp5Q7shkhp5ZpxFVk8TGDL',
 'fitness': ['01', '0000000000074230'],
 'context': 'CoV64qaFykBGjNzStJDdTu7NyCPrmPVMPRmFFHVYrdzS1zrGxiZD',
 'priority': 0,
 'proof_of_work_nonce': 'ba45727c3e590600',
 'signature': 'siga8iyjVFyKDkycPSSX47hs4S9byVPEtScXsL5C82yuWD1Xm4qDDKkJvUTjuQUhE5Uwk4NiqiXHfQq53EoRwwrUJpxxQ9vA'}

Block header fields are also divided into "shell" (protocol independent) and protocol-specific.
Read detailed description of each field in the whitedoc

A useful source of information is the block receipt containing state changes happened in the result of applying this block. There are also operation receipts which we will examine a bit later.

{'protocol': 'PtCJ7pwoxe8JasnHY8YonnLYjcVHmhiARPJvqcC6VfHT5s8k8sY',
 'next_protocol': 'PtCJ7pwoxe8JasnHY8YonnLYjcVHmhiARPJvqcC6VfHT5s8k8sY',
 'test_chain_status': {'status': 'not_running'},
 'max_operations_ttl': 60,
 'max_operation_data_length': 16384,
 'max_block_header_length': 238,
 'max_operation_list_length': [{'max_size': 32768, 'max_op': 32},
  {'max_size': 32768},
  {'max_size': 135168, 'max_op': 132},
  {'max_size': 524288}],
 'baker': 'tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9',
 'level': {'level': 10000,
  'level_position': 9999,
  'cycle': 2,
  'cycle_position': 1807,
  'voting_period': 0,
  'voting_period_position': 9999,
  'expected_commitment': False},
 'voting_period_kind': 'proposal',
 'nonce_hash': None,
 'consumed_gas': '0',
 'deactivated': [],
 'balance_updates': [{'kind': 'contract',
   'contract': 'tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9',
   'change': '-16000000'},
  {'kind': 'freezer',
   'category': 'deposits',
   'delegate': 'tz3RDC3Jdn4j15J7bBHZd29EUee9gVB1CxD9',
   'level': 2,
   'change': '16000000'}]}

In order to better understand what receipt fields mean, check out the articles about Proof-of-stake in Tezos and how self-amendmend works.
There are also several shortcuts for most common cases:


Lastly, you can examine some rare and interesting cases, e.g. protocol activation:

protocol_parameters = pytezos.shell.blocks[1].protocol_parameters()  # genesis block
protocol_parameters['commitments'][:5]  # recomended allocations (aka commitments)
[['btz1LKddKfC9cEitY8Q5bMnGS9yj9uPWnmX68', '4974860242'],
 ['btz1LKj1hxiXZUkZGuCMTNtGos8npjo9VjFnJ', '14770866000'],
 ['btz1LKkKTQ34S1BSrcEwYFpVRhm2inDYfjCMg', '1551209817'],
 ['btz1LKmBsCgNKpkWq3hBBqrPPyEmE1XegouD8', '12000000000'],
 ['btz1LKnYAGSyGRJiY7pieJVK9RR6v3e2ko6QS', '9000000000']]

# Block slices, cycles, and voting periods

This not a wrapped RPC endpoint but a convenient extension allowing to manipulate with block slices and iterate over larger timeframes namely cycles and voting periods.


Block slice has get_range method that returns boundary levels which can be useful when working with cycles/voting periods.

(1130497, 1131056)
(1, 32768)

The main purpose of block slices (aside from determining boundaries) is operation search, e.g. when you have recently sent an operation and want to find it by hash:

{'protocol': 'PsCARTHAGazKbHtnKfLzQg3kms52kSRpgnDY982a9oYsSXRLQEb',
 'chain_id': 'NetXdQprcVkpaWU',
 'hash': 'onw8xuA8SKNdJo4VUf5vyhJKRmggWRVuLAavCbRnmQvE1zr65KF',
 'branch': 'BLCWACrJjKFGEwUecZQjna3eiWCy21xCz1cu3TdpmSiLN1KCmJz',
 'contents': [{'kind': 'transaction',
   'source': 'tz1bNL8YciKPtCuKNzQWxVF8Bnm1h3sd8sbB',
   'fee': '8113',
   'counter': '5373301',
   'gas_limit': '78140',
   'storage_limit': '0',
   'amount': '0',
   'destination': 'KT1VG2WtYdSWz5E7chTeAdDPZNy2MpP8pTfL',
   'parameters': {'entrypoint': 'default',
    'value': {'prim': 'Right',
     'args': [{'prim': 'Left',
       'args': [{'bytes': 'c139928f72887b79db36eb848dbe78aafa429c980421cc7e88a6d117a22949b1'}]}]}},
   'metadata': {'balance_updates': [{'kind': 'contract',
      'contract': 'tz1bNL8YciKPtCuKNzQWxVF8Bnm1h3sd8sbB',
      'change': '-8113'},
     {'kind': 'freezer',
      'category': 'fees',
      'delegate': 'tz1ei4WtWEMEJekSv8qDnu9PExG6Q8HgRGr3',
      'cycle': 275,
      'change': '8113'}],
    'operation_result': {'status': 'applied',
     'storage': {'prim': 'Pair', 'args': [{'int': '4'}, {'prim': 'Unit'}]},
     'big_map_diff': [{'action': 'update',
       'big_map': '4',
       'key_hash': 'exprtfWG91BKChtLpWyNfQCyCzUfkXWCT6CQp2rVYoN5UWGdfRp5gP',
       'key': {'bytes': '637a015a2ef8bf984c1c3d9c764afede725dc8e309b24706e9d3333a77f4ae0c'}}],
     'consumed_gas': '67833',
     'storage_size': '1859'},
    'internal_operation_results': [{'kind': 'transaction',
      'source': 'KT1VG2WtYdSWz5E7chTeAdDPZNy2MpP8pTfL',
      'nonce': 0,
      'amount': '1505762090',
      'destination': 'tz1bNL8YciKPtCuKNzQWxVF8Bnm1h3sd8sbB',
      'result': {'status': 'applied',
       'balance_updates': [{'kind': 'contract',
         'contract': 'KT1VG2WtYdSWz5E7chTeAdDPZNy2MpP8pTfL',
         'change': '-1505762090'},
        {'kind': 'contract',
         'contract': 'tz1bNL8YciKPtCuKNzQWxVF8Bnm1h3sd8sbB',
         'change': '1505762090'}],
       'consumed_gas': '10207'}}]}}],
 'signature': 'sigY3K6ZYBg4QbzoC1XUqq6UqT1yTLxyh5uge9ixmbGpNTH7P2BZxRphXzAY7hTDNTx8EiUzundyQMNYxoUZ7Tzus3RRqpKs'}

# Operation group

If we take a look at the list of operations included into the block we will find a list of lists actually, moreover the size of the outer list is always 4.


These are the so-called validation passes, the operations are grouped in such a way as to detect the error as early as possible.

  1. Endorsement — contains only operations with kind "endorsement", which affect the fitness of the block;
  2. Vote — contains "ballot" and "proposal" operations used in the self-amending mechanism;
  3. Anonymous — contains operations that do not have signatures thus cannot be linked to any particular account (until some level);
  4. Manager — contains signed operations that do change account states.

Read more about various operation kinds.

<pytezos.rpc.protocol.OperationListListQuery object at 0x7fcf3b595588>

Properties .path # /chains/main/blocks/head/operations .node # https://mainnet-tezos.giganode.io/ ()

() All the operations included in the block. :returns: Array

[] Find operation by hash.

:param item: Operation group hash (base58) :rtype: OperationQuery

Helpers .anonymous .endorsements .find_ballots() .find_origination() .find_upvotes() .managers .votes

Note shortcuts for validation passes, also you can search through all operations by hash.

So far we used the term "operation" although were actually speaking about "operation groups". Here's how an operation group is constructed:

Operation Group  <-- hash refers to the entire group
└ Content #0
  └ Main operation
  └ Internal operation results  <-- only if main operation is "manager" and applied
    └ Internal operation #0
    └ Internal operation #m
└ Content #n

Multicontent structure essentially allows operation batching, however an important rule must be followed: only operations with the same validation pass can be batched.

Internal operations may occur as a result of calls to smart contracts, which may invoke other contracts in their turn. Pay attention to the flat structure (intuitively expected tree-like) of internal operations. In Tezos contracts cannot synchronously invoke other contracts but return a list of spawned operations (i.e. async cross-contract calls) instread. The ordering of contract calls is quite unusual, BFS is used (usually DFS):

A      1
└ B    2
  └ C  4
  └ D  5
  E    3
  └ F  6
  └ G  7

How to determine the order of a particular operation from the RPC response:

  1. Main operations — by counter, which is tied to a specific account (signatory) and is incremented by one after each successful operation;
  2. Internal operations — by nonce, which is being reset on every main operation.

PyTezos provides convenient iterators for operations and operation results (receipts):

from pytezos.operation.result import OperationResult
opg = pytezos.shell \
    .blocks[1113876] \
for op in OperationResult.iter_contents(opg):
    print(op['kind'], 'internal' if op['internal'] else '')
transaction transaction internal transaction internal
for res in OperationResult.iter_results(opg):
applied applied applied

You can also aggregate some stats or extract specific fields:

opg = pytezos.shell \
    .blocks[850141] \


# Context

Context is the blockchain state, it keeps actual information about accounts, balances, contract data, and other. Note, that a node has to run in a special "archive" mode in order to provide access to the context. Also note that many data providers close particularly heavy context endpoints.

The most common scenario using context data is querying account state (balance, storage, code, baker-specific info):

{'balance': '24969932',
 'delegate': 'tz1NortRftucvAkD1J58L32EhSVrQEWJCEnB',
 'counter': '6761142'}
{'prim': 'Pair', 'args': [{'int': '4'}, {'prim': 'Unit'}]}

There is no public documentation for context endpoints, but you can utilize the one integrated to PyTezos:

<pytezos.rpc.protocol.ContextRawJsonQuery object at 0x7fcf48d4b4e0>

Properties .path # /chains/main/blocks/head/context/raw/json .node # https://mainnet-tezos.giganode.io/ ()

() ¯_(ツ)/¯ :param depth: ¯_(ツ)/¯ :returns: Object

RPC endpoints .active_delegates_with_rolls .big_maps .block_priority .commitments .contracts .cycle .delegates .delegates_with_frozen_balance .ramp_up .rolls .votes

For example, you can query all keys of a particular big map:


# Boosting RPC

PyTezos uses several tricks to improve experience with RPC requests. First, it caches deterministic requests (i.e. to past blocks) but only for known public networks. This option can be turned off:

pytezos.shell.node.caching = False

In addition to that, you can use a pool of RPC nodes to make concurrent requests:

from concurrent.futures import ThreadPoolExecutor
import logging


registry = pytezos.using('mainnet-pool') \

bakers_addresses = ['tz1abmz7jiCV2GH2u81LRrGgAFFgvQgiDiaf', 

def big_map_get(address):
    return registry.big_map_get(address, 'head')

with ThreadPoolExecutor(max_workers=3) as executor:
    snapshot = executor.map(big_map_get, bakers_addresses)
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): rpc.tzkt.io:443 DEBUG:urllib3.connectionpool:https://rpc.tzkt.io:443 "GET /mainnet/chains/main/blocks/head/context/contracts/KT1ChNsEFxwyCbJyWGSL3KdjeXE28AY1Kaog HTTP/1.1" 200 None DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): tezos-prod.cryptonomic-infra.tech:443 DEBUG:urllib3.connectionpool:Starting new HTTPS connection (2): tezos-prod.cryptonomic-infra.tech:443 DEBUG:urllib3.connectionpool:Starting new HTTPS connection (3): tezos-prod.cryptonomic-infra.tech:443 DEBUG:urllib3.connectionpool:https://tezos-prod.cryptonomic-infra.tech:443 "GET /chains/main/blocks/head/context/contracts/KT1ChNsEFxwyCbJyWGSL3KdjeXE28AY1Kaog/storage HTTP/1.1" 200 None DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): rpc.tezrpc.me:443 DEBUG:urllib3.connectionpool:https://tezos-prod.cryptonomic-infra.tech:443 "GET /chains/main/blocks/head/context/contracts/KT1ChNsEFxwyCbJyWGSL3KdjeXE28AY1Kaog/storage HTTP/1.1" 200 None DEBUG:urllib3.connectionpool:https://tezos-prod.cryptonomic-infra.tech:443 "GET /chains/main/blocks/head/context/contracts/KT1ChNsEFxwyCbJyWGSL3KdjeXE28AY1Kaog/storage HTTP/1.1" 200 None DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.tezos.org.ua:443 DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.tez.ie:443 DEBUG:urllib3.connectionpool:https://rpc.tezrpc.me:443 "POST /chains/main/blocks/head/context/contracts/KT1ChNsEFxwyCbJyWGSL3KdjeXE28AY1Kaog/big_map_get HTTP/1.1" 200 None DEBUG:urllib3.connectionpool:https://api.tezos.org.ua:443 "POST /chains/main/blocks/head/context/contracts/KT1ChNsEFxwyCbJyWGSL3KdjeXE28AY1Kaog/big_map_get HTTP/1.1" 301 None DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): mainnet-tezos.giganode.io:443 DEBUG:urllib3.connectionpool:https://api.tez.ie:443 "POST /chains/main/blocks/head/context/contracts/KT1ChNsEFxwyCbJyWGSL3KdjeXE28AY1Kaog/big_map_get HTTP/1.1" 200 819 DEBUG:urllib3.connectionpool:https://mainnet-tezos.giganode.io:443 "GET /chains/main/blocks/head/context/contracts/KT1ChNsEFxwyCbJyWGSL3KdjeXE28AY1Kaog/big_map_get HTTP/1.1" 405 None
add add