Skip to content

Instantly share code, notes, and snippets.

@tomsib2001
Last active February 20, 2020 09:55
Show Gist options
  • Save tomsib2001/332345430b650cc32d0e2d6645a35a39 to your computer and use it in GitHub Desktop.
Save tomsib2001/332345430b650cc32d0e2d6645a35a39 to your computer and use it in GitHub Desktop.

Sapling is coming

When Bitcoin was designed by Satoshi Nakamoto, he was mainly concerned with achieving consensus between peers so as to establish a credible digital currency. Anonymity -- much like in the case of the Internet -- was all the less a worry that Bitcoin addresses provide pseudonymity: until bitcoins are transferred to or from a cryptocurrency exchange, it is difficult to link them to a real-world identity. Nowadays, it is extremely difficult to remain anonymous on public blockchains such as Bitcoin, which poses privacy concerns. Fortunately, a solution to this issue has been coming from the very science that made Bitcoin possible: cryptography. The Zcash cryptocurrency pioneered the use of zero-knowledge cryptography on the blockchain. In short, zero-knowledge cryptography allows one to prove they possess the solution to a specific problem of fixed size without revealing the solution. In the context of blockchains, this means the possibility to build transactions which can be verified correct without revealing its source, amount or destination to anyone but the parties involved.

Zero-knowledge cryptography on the blockchain has become a lot more practical thanks to amazing work by the ZCash team, called 'Sapling'. We are excited to learn that the sapling framework is being ported to Tezos. This post introduces a shielded_tez contract in the Michelson language allowing for private transactions. Concretely, you the user can create a "shielded" account with a special address (starting with "zet") to which you can transfer tezzies using a new shield command. You may then transfer funds to other shielded adresses using zero-knowledge transactions which do not reveal either the sender, amount, or destination addresses. At any point in time, you may choose to unshield a part or the entirety of your shielded funds to any Tezos address. Provided this new address is not related to the one you used to shield funds, this arrangement allows for anonymity[1].

Elegantly, all this is achieved by introducing only two types and one instruction.

At Origin Labs, we were curious to see how easy it would be to:

  1. port sapling to our homegrown Liquidity language;
  2. push the technology further thanks to the expressiveness of Liquidity.

We achieved both and in little time, we were able to build a mixer for ERC20-like tokens, which is to say the possibility to shield and unshield such tokens in a way similar to what shielded_tez does with XTZ. This speaks to the strength and expressiveness of our decompilation tool (Michelson to Liquidity) and the adaptiveness of Liquidity: it was straightforward to add the new types and instruction and to develop the mixer smart contract.

Below we go into more technical detail.

Sapling in Liquidity

Part 1. was quite straightforward. Recall that sapling introduces the two types sapling_state and sapling_transaction, as well as the sapling_verify_update instruction. After decompiling the shielded_tez.tz contract, we got the following Liquidity program:

type storage = sapling_state


[%%entry
  let main (tr, dest_opt)
    (storage : storage) =

    let (balance,opt_st) =
      sapling_verify_update tr storage "SaplingForTezosV1"  in
    let balance_tez =
      0.000001DUN *
        ([%nat  match balance with
           | Plus exp9 -> exp9
           | Minus exp9 -> exp9])
       in
    if (balance) > 0
    (* case when the transfer unshields money (z_to_t) *)
    then
      let dest =
        match dest_opt with
        | None  -> failwith ()
        | Some dest -> dest  in
      let transfer = Account.transfer ~dest:dest ~amount:balance_tez  in
      ([transfer],
        (match opt_st with
         | None  -> failwith ()
         | Some st -> st))
    else
    (* case when the transfer either shields money (t_to_z)
    or does a shielded transaction (z_to_z) *)
      (if 0DUN = ((Current.amount ()) - balance_tez) then () else failwith ();
       (match dest_opt with | None  -> () | Some _ -> failwith ());
       (([] : operation list),
         ((match opt_st with
           | None  -> failwith ()
           | Some st -> st))))
    ]

Although this contract could still be improved aesthetically, it is somewhat easier to follow than the Michelson version:

storage sapling_state;
parameter (pair sapling_transaction (option key_hash) );
code { UNPAIR ;
       UNPAIR ;
       DIP { SWAP ;
             DIP {PUSH string "SaplingForTezosV1" ;} ;} ;
       SAPLING_VERIFY_UPDATE ;
       DUP ;
       DIP{ ABS;
            PUSH mutez 1;
            MUL};
       IFGT { DIP { SWAP;
                    ASSERT_SOME;
                    IMPLICIT_ACCOUNT};
              UNIT;
              TRANSFER_TOKENS;
              NIL operation;
              SWAP;
              CONS;
              DIP { ASSERT_SOME;}}
            {
              AMOUNT;
              SUB;
              PUSH mutez 0;
              ASSERT_CMPEQ;
              ASSERT_SOME;
              DIP { ASSERT_NONE;};
              NIL operation};
       PAIR}

Building a mixer for tokens

Building on this success, we decided to explore something which would have been quite painful to build using only Michelson: a mixer for ERC20-like tokens. Using such a contract, one would deposit their tokens, and unlinkably withdraw them to protect their anonymity. For this we needed some minor client-side tweaks of the sapling prototype of the Tezos code.

Storage

Our mixer contract is rather simple (the full code is available at the end of the article). Its storage is

type storage =
{
  sapling_state : sapling_state;
  erc20addr : address;
}

The sapling state can be seen as the "masked database" containing the private transactions and balances: to the outside observer it is of no more use than a random sequence of bytes. The address of the token contract is hardcoded in the mixer.

Methods

The mixer contract has two entrypoints: receiveTokens and withdraw.

receiveTokens conforms with our specification for token contracts and receives tokens from the token contract, along with a sapling transaction which shields the corresponding quantity of tokens.

withdraw enables one to recover tokens previously shielded and send them to any account (preferably not the one with which they were shielded).

There is a method deposit which is not an entrypoint. It is triggered by a successful call to receiveTokens if the included sapling transaction is deemed correct.

A closer technical view of our mixer contract

Below is the full code of our prototype mixer contract. First, note that this contract needs to implement the tokenreceiver interface:

contract type TokenReceiver = sig
  type storage
  val%entry receiveTokens : (address * nat * bytes option) -> _
  (* more ... *)
end

Here is the actual contract:

[%%version 2.0]



type storage =
{
  sapling_state : sapling_state;
  erc20addr : address;
}

let%init storage =
{
  sapling_state = (0x : sapling_state);
  erc20addr = KT1SAaFjYUD5KFYidYxPzpnf6HgFs4oAJuTz;
}

(* point of entry for shielding tokens *)
[%%entry
let receiveTokens ((_ : address), tokens, (bytes_opt : bytes option)) storage =
  if Current.sender () <> storage.erc20addr then
    failwith ("Wrong ERC contract sent the tokens",Current.sender(),storage.erc20addr)
  else
    match bytes_opt with
    | None -> failwith "No sapling transaction"
    | Some tr ->
      match (Bytes.unpack tr : sapling_transaction option) with
      | None -> failwith "Bad argument type, should be sapling transaction."
      | Some tr ->
  deposit tr tokens storage
]

(* Note that this is not an entry point: it is called by
   receiveTokens if the sapling transaction is valid. *)
let deposit (tr : sapling_transaction) (tokens:nat) storage =
  let (balance,opt_st) = sapling_verify_update tr storage.sapling_state
      "SaplingForTezosV1" in
  let balance =
    [%nat  match balance with
      | Plus balance ->
          if balance = 0p then
            balance
          else
            failwith ("should not be unshielding in deposit",balance)
      | Minus balance -> balance] in
  if balance <> tokens then
    failwith ("Misleading amount in sapling transaction.",balance,tokens)
  else
    let st =
      match opt_st with
      | None  -> failwith ("invalid shielded transaction")
      | Some st -> st in
    let storage = storage.sapling_state <- st in
    [],storage

[%%entry

let withdraw ((tr : sapling_transaction),(dest: bytes option)) storage =
    let (balance,opt_st) = sapling_verify_update tr storage.sapling_state
    "SaplingForTezosV1" in
    let balance =
    [%nat  match balance with
      | Minus _ -> failwith (balance,"should not be shielding in withdraw")
      | Plus balance -> balance] in
    let st =
    match opt_st with
      | None  -> failwith "invalid shielded transaction"
      | Some st -> st in
    let storage = storage.sapling_state <- st in
    [
     storage.erc20addr.transfer
     (Current.sender (),balance, (None : bytes option))
     ~amount:0dun
    ],storage

]

Conclusion

We are excited to add to current contributions in the current push for anonymity in the blockchain community. The high-level expressiveness of Liquidity allowed us to quickly iterate over the existing prototype by Nomadic Labs. As proposals stabilize, we will start releasing stable code to the broader community, stay tuned for more!

  1. Of course, there need to be other users of the contract, and the amount being shielded and then unshielded must not be too specific. Moreover, since shielded transactions are submitted to the contract, the account submitting them needs to be unconnected to the shielding/unshielding address.
@mebsout
Copy link

mebsout commented Feb 14, 2020

over the existing prototype over at Nomadic Labs -> over the existing prototype of Nomadic Labs

@mebsout
Copy link

mebsout commented Feb 14, 2020

course, there need to be other -> course, there needs to be other

@mebsout
Copy link

mebsout commented Feb 14, 2020

La première partie de l'intro est super, mais on passe rapidement à sapling et après ça devient très technique. Est-ce que tu penses que ça aurait du sens de rajouter une petite partie pour expliquer sapling pour les gens qui ne savent pas lire le code et les explications qui suivent (tu en parles après mais ces gens là n'iront pas jusqu'à cette partie de l'article).

Aussi il n'y a pas de commentaires sur le code du mixer. En gros il faudrait rajouter qqes explications (ou un schéma) sur comment la base de donnée cachée est utilisée et un genre de mini manuel pour décrire les points d'entrée.

@mebsout
Copy link

mebsout commented Feb 14, 2020

Aussi enlever le code commenté non utilisé dans le mixer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment