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:
- port
sapling
to our homegrown Liquidity language; - 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.
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 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.
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.
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.
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
]
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!
- 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.
over the existing prototype over at Nomadic Labs -> over the existing prototype of Nomadic Labs