Atomic swap tips: Berlin Tezos workshop materials

Tezos Atomic swap workshop at the TQuorum event in Berlin. It is easy to interact with Tezos using PyTezos Python library.

Michael
Michael   Follow

Last week we held a workshop at the fantastic TQuorum event in Berlin. Although not everything went smoothly, we hope that we were able to convey the main point: it is super easy to interact with Tezos using our Python toolset.

  • Setting up an environment is not necessary, just use cloud Jupyter notebooks;
  • Requires a minimum of developer skills, interactive documentation will back you up till the end;
  • Pre-populated credentials and keys (demo purposes only) allow to start in seconds.

In this post, we will recall the idea and the scope of the application we developed. We also attached a slightly modified notebook so that you can reproduce the experiment at any time.

# Atomic swaps basics

The term “atomic swap” has a fairly general definition, and in this workshop, we will mean the following: two parties exchange two different types of assets, and these transactions must be either successful or unsuccessful.

Atomic swap solves the trust issue

Atomic swap solves the trust issue between parties. In the real world, it is implemented by introducing a trusted third-party, for instance, a bank or government. In the blockchain universe, we can replace third-party by a smart contract and in the result, we have a fully trustless way to exchange assets.

But as usual, things are not so simple. The swap protocol is interactive, which means that it is not enough to initialize and send transactions correctly, but also to perform some actions in a timely manner.

Drastically simplified XTZ/ETH swap timeline

You can see at the simplified timeline that the swap initiator (Michel) has to act twice with a delay in time. He is also limited to a relatively short interval, and there is a non-zero possibility of losing his funds if for some reason there is no access to the blockchain at that moment.

Good news is that the last task can be fulfilled by a third party while the protocol remains secure. A small reward is paid to the anonymous hero, so there is an economic incentive to secure the atomic swap and a sort of who-is-faster challenge.

# Implementation details

We will use an atomic swap implementation by Atomex.me. Let’s give a look at their contract in Tezos alphanet. There are three main entrypoints that change current swap state and optionally transfer funds.

  • initiate adds new big_map entry with all data required for the swap protocol, sha256(sha256(secret)) is used as a key;
  • redeem removes big_map entry and transfers money to the participant (he needs to know secret);
  • refund if something goes wrong you can rollback;

For the sake of simplicity, we will act for two parties and don’t request any data from other blockchains. Here is the plan of what we’ll do:

  1. Generate a secret, create a new swap using initiate entry (as initiator);
  2. Get active swaps i.e. existing big_map entries (as third-party)
  3. Make a redeem using the secret we generated as if we extracted it from another blockchain.

# Practical work

We are now ready to move on to the practical part, first let’s install all the necessary packets:

!apt-get install libsodium-dev libsecp256k1-dev libgmp-dev
!pip install pytezos conseil

Then we create a new swap using PyTezos.

Here is our operation in better-call.dev explorer:

import os
from hashlib import sha256
from pytezos import pytezos
from datetime import datetime, timedelta
from decimal import Decimal
secret = os.urandom(32)
secret
result
b'\xbf\xbb\xb1o\xdd\x0f\x82,\x82\xe4\x90\xe1\x04\xc2d\x1b\x7f\x96|j\x87\x05\xba=\xbf\x15.\x9d7G\x13\x17'
secret_hash = sha256(sha256(secret).digest()).hexdigest()
secret_hash
result
'12f64fbad3843c5803aab31f8030304e4ba7688e5b56d39dc6abe8bb3a25254e'
ci = pytezos.contract('KT1WhouvVKZFH94VXj9pa8v4szvfrBwXoBUj')
ci.ll
result

<pytezos.michelson.interface.ContractEntrypoint object at 0x7ff3e64d6eb8>

Properties .key # tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm .shell # https://tezos-dev.cryptonomic-infra.tech/ (alphanet) .address # KT1WhouvVKZFH94VXj9pa8v4szvfrBwXoBUj

$kwargs: { "participant": $address, "settings": $settings }

$settings: { "hashed_secret": $bytes, "refund_time": $timestamp, "payoff": $mutez }

$mutez: int /* Amount in `utz` (10^-6) */ || Decimal /* Amount in `tz` */

$address: string /* Base58 encoded `tz` or `KT` address */

$bytes: string /* Hex string */ || bytes /* Python byte string */

$timestamp: int /* Unix time in seconds */ || string /* Formatted datetime `%Y-%m-%dT%H:%M:%SZ` */

initiate = ci.ll(
    participant=pytezos.key.public_key_hash(),
    settings=dict(
        hashed_secret=secret_hash,
        refund_time=int((datetime.utcnow() + timedelta(6)).timestamp()),
        payoff=Decimal('0.01')
    )
).with_amount(Decimal('1'))
initiate.result().big_map_diff
result
{'12f64fbad3843c5803aab31f8030304e4ba7688e5b56d39dc6abe8bb3a25254e': {'recipients': {'initiator': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm', 'participant': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm'}, 'settings': {'amount': Decimal('0.99'), 'refund\_time': 1567417055, 'payoff': Decimal('0.01')}}}
initiate.inject()
result
'ookSqpsFiHKwjFzfD9VP5zreQ7PWBREcNwkUpmd5eWRjHGwRpQ8'

Next, we need to get all recent big_map entries. We will use ConseilPy to get pairs (block_level, operation_group_hash) so we can request big_map_diff with PyTezos afterward.

from conseil import conseil
from pytezos import pytezos
from datetime import datetime, timedelta
Operation = conseil.tezos.alphanet.operations
Operation
result

Path metadata/tezos/alphanet/operations/attributes

Attributes .operation_group_hash .kind .level .delegate .slots .nonce .pkh .secret .source .fee .counter .gas_limit .storage_limit .public_key .amount .destination .parameters .manager_pubkey .balance .spendable .delegatable .script .storage .status .consumed_gas .storage_size .paid_storage_size_diff .originated_contracts .block_hash .block_level .timestamp .internal

txs = Operation.query(Operation.block_level, Operation.operation_group_hash) \
    .filter(Operation.destination == 'KT1WhouvVKZFH94VXj9pa8v4szvfrBwXoBUj',
            Operation.status == 'applied',
            Operation.timestamp > 1000 * int((datetime.utcnow() - timedelta(3)).timestamp())) \
    .order_by(Operation.timestamp) \
    .all()
txs
result[{'block\_level': 609940,
  'operation\_group\_hash': 'ookSqpsFiHKwjFzfD9VP5zreQ7PWBREcNwkUpmd5eWRjHGwRpQ8'}]
op = pytezos.shell.blocks[txs[0]['block_level']].operations[txs[0]['operation_group_hash']]()
op
result{'protocol': 'Pt24m4xiPbLDhVgVfABUjirbmda3yohdN82Sp9FeuAXJ4eV9otd',
 'chain\_id': 'NetXgtSLGNJvNye',
 'hash': 'ookSqpsFiHKwjFzfD9VP5zreQ7PWBREcNwkUpmd5eWRjHGwRpQ8',
 'branch': 'BKnC1eLAicw5Zfgj48cvrm2P3XQjRu7BrH1tmWCUywuHb28LyuG',
 'contents': [{'kind': 'transaction',
   'source': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm',
   'fee': '7623',
   'counter': '233596',
   'gas\_limit': '72637',
   'storage\_limit': '0',
   'amount': '1000000',
   'destination': 'KT1WhouvVKZFH94VXj9pa8v4szvfrBwXoBUj',
   'parameters': {'prim': 'Left',
    'args': [{'prim': 'Left',
      'args': [{'prim': 'Pair',
        'args': [{'string': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm'},
         {'prim': 'Pair',
          'args': [{'prim': 'Pair',
            'args': [{'bytes': '12f64fbad3843c5803aab31f8030304e4ba7688e5b56d39dc6abe8bb3a25254e'},
             {'int': '1567417055'}]},
           {'int': '10000'}]}]}]}]},
   'metadata': {'balance\_updates': [{'kind': 'contract',
      'contract': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm',
      'change': '-7623'},
     {'kind': 'freezer',
      'category': 'fees',
      'delegate': 'tz3gN8NTLNLJg5KRsUU47NHNVHbdhcFXjjaB',
      'cycle': 297,
      'change': '7623'}],
    'operation\_result': {'status': 'applied',
     'storage': {'prim': 'Pair', 'args': [[], {'prim': 'Unit'}]},
     'big\_map\_diff': [{'key\_hash': 'exprvN5C4dTXtaq5JRb3g8NBUHssLjSAJRQx6Pb8vHLQVNyLnSaCb8',
       'key': {'bytes': '12f64fbad3843c5803aab31f8030304e4ba7688e5b56d39dc6abe8bb3a25254e'},
       'value': {'prim': 'Pair',
        'args': [{'prim': 'Pair',
          'args': [{'bytes': '0000e8b36c80efb51ec85a14562426049aa182a3ce38'},
           {'bytes': '0000e8b36c80efb51ec85a14562426049aa182a3ce38'}]},
         {'prim': 'Pair',
          'args': [{'prim': 'Pair',
            'args': [{'int': '990000'}, {'int': '1567417055'}]},
           {'int': '10000'}]}]}}],
     'balance\_updates': [{'kind': 'contract',
       'contract': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm',
       'change': '-1000000'},
      {'kind': 'contract',
       'contract': 'KT1WhouvVKZFH94VXj9pa8v4szvfrBwXoBUj',
       'change': '1000000'}],
     'consumed\_gas': '72537',
     'storage\_size': '3315'}}}],
 'signature': 'sigZU1K4hW5KVC2cpxmQHcwa3qLvihcnUNb1XvAte6yipxhrY6YJNvZcrBPbGAyusfngvEob4LDt5SDPsmV1Qu7BkKThhgGQ'}
ci = pytezos.contract('KT1WhouvVKZFH94VXj9pa8v4szvfrBwXoBUj')
ci.operation_result(op).big_map_diff
result{'12f64fbad3843c5803aab31f8030304e4ba7688e5b56d39dc6abe8bb3a25254e': {'recipients': {'initiator': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm',
   'participant': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm'},
  'settings': {'amount': Decimal('0.99'),
   'refund\_time': 1567417055,
   'payoff': Decimal('0.01')}}}
big_map = dict()
for tx in txs:
    opg = pytezos.shell.blocks[tx['block_level']].operations[tx['operation_group_hash']]()
    for k, v in ci.operation_result(opg).big_map_diff.items():
        if v and v['settings']['payoff'] > 0:
            big_map[k] = v
big_map
result{'12f64fbad3843c5803aab31f8030304e4ba7688e5b56d39dc6abe8bb3a25254e': {'recipients': {'initiator': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm',
   'participant': 'tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm'},
  'settings': {'amount': Decimal('0.99'),
   'refund\_time': 1567417055,
   'payoff': Decimal('0.01')}}}

Finally, we can make a redeem, but we should do that using another identity (key). Go to https://faucet.tzalpha.net/ and download a json file with key data.

from pytezos import pytezos
secret = b'\xbf\xbb\xb1o\xdd\x0f\x82,\x82\xe4\x90\xe1\x04\xc2d\x1b\x7f\x96|j\x87\x05\xba=\xbf\x15.\x9d7G\x13\x17'
ci = pytezos.contract('KT1WhouvVKZFH94VXj9pa8v4szvfrBwXoBUj')
ci.contract.parameter
result

<pytezos.michelson.contract.ContractParameter object at 0x7fb061a03f98>

$parameter: { "ll": $ll } || { "lr": $bytes /* hashed_secret */ } || { "rl": $bytes /* secret */ } || { "rr": $bytes /* hashed_secret */ }

$ll: { "participant": $address, "settings": $settings }

$settings: { "hashed_secret": $bytes, "refund_time": $timestamp, "payoff": $mutez }

$timestamp: int /* Unix time in seconds */ || string /* Formatted datetime `%Y-%m-%dT%H:%M:%SZ` */

$bytes: string /* Hex string */ || bytes /* Python byte string */

$address: string /* Base58 encoded `tz` or `KT` address */

$mutez: int /* Amount in `utz` (10^-6) */ || Decimal /* Amount in `tz` */

Helpers .decode() .encode() .entries()

redeem = ci.using(key='~/Downloads/tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa.json').rl(secret)
redeem.result().big_map_diff
result{'12f64fbad3843c5803aab31f8030304e4ba7688e5b56d39dc6abe8bb3a25254e': None}
redeem.inject()
result'oomeWo4umG8PCdjckVkKvFHc8ByBo3aA2FeQSsNdP43dyygNpYm'

It works! Note that there are two internal transactions and one of them is our tips:

# Learn more about our toolset

# PyTezos

Python SDK for Tezos, providing functional for querying RPC endpoints, sending operations, interacting with smart contracts, working with cryptographic keys.

# ConseilPy

Python wrapper for the Conseil blockchain indexer by Cryptonomic providing access to the indexed Tezos blockchain data.

# Better Call Dev

Michelson contract explorer displaying transaction parameters, storage and big_map diffs in a convenient format.

Follow us on Twitter and join our cozy Telegram chat, where you can ask any question about our products and services.


Join the first Eastern European Tezos Hackathon devoted to the non-fungible token standard for the Tezos platform. It’s gonna be three incredible days (6–8 Sep) with the Tezos community in Kyiv.

See you there!