Liquidity Tutorials — Hands-on Series — Part 4

This is the fourth part of a tutorial series on writing smart contracts in Liquidity. I recommend going through the first, second, and third parts if you haven’t already.

In the last tutorial, we wrote a contract that publishes authenticated data. The contract ensures that the published data is from the publisher and not anyone else. However, because the data is stored in the contract storage, it gets costly to publish large data. In this tutorial, we’ll write a different implementation of the same contract, altered such that large amounts of data can be published without high cost.

We make use of a hash function to hash the data to be published. A nice property of hash functions is that they can map data of arbitrary size to a fixed size. For large data, the contract can save costs by storing the hashed data instead of the plain data. The readable data can be stored somewhere else off the chain. To make sure the off-chain data is not compromised, the contract verifies the data for the audience. When the audience calls the contract, he/she provides the data he/she has as a parameter. The contract then hashes the data provided and checks it against the hashed data that is in the storage of the contract.

Note that if the hash function utilized is collision resistant, it is also safe. That is, it is computationally hard to find a second, different piece of data which hashes to the same value as the hash of the originally published data.

To summarize:

The data publisher:

  • publishes the data off-chain (e.g., on a website).
  • hashes the published data.
  • deploys the contract, with the hashed data as the initial storage.
  • calls the contract to update data, with the new data as the parameter.

The audience:

  • calls the contract, with the plain data as the parameter, to verify the data.

The cost of calling the contract does not depend on the size of the parameter, only on the size of the storage. The plain data (parameter) can thus be arbitrarily large without cost concerns. Because the data is stored in hashed form, whenever the publisher updates the data the new storage will be the same fixed size (dependent on the chosen hash function) and thus the publisher does not need to pay for extra storage even if the updated data is larger.

Let’s start writing the contract!

I saved the contract as data_publisher_hash.liq:

type storage = {
publisher : address;
data : bytes
}

let%entry main (param : string) storage =
if (Crypto.sha512 (Bytes.pack param)) = storage.data then
if Current.amount() < 1tz then
failwith "Not enough money, queries cost 1 tez."
else
([], storage)
else if Current.sender () <> storage.publisher then
failwith "Cannot authenticate."
else
([], storage.data <- Crypto.sha512 (Bytes.pack param))

The storage is a record with two fields:

  • the publisher field contains the address of the publisher (of type address).
  • the data field contains the hashed form of the data to be published (of type bytes). Any data type can be hashed and published.

The parameter is a string, legitimately, it can be

  • a string of the published data when a member of the audience calls the contract to verify the data.
  • a string of the updated data when the publisher calls the contract to update the data.

There is only one entry point, called main.

  • its inputs are:
  1. the parameter of the contract, which I named param, of type string.
  2. the storage, which I named storage, of type storage, i.e., a record with the publisher address and the data fields.
  • its output, as always, is a pair of operation list and storage. The output depends on the result of the if-statement that compares the hashed form of param to the data field of the storage.

Once a parameter is passed in, we pack the data to bytes, then hash the bytes, using the following functions:

  • Bytes.pack: 'a -> bytes. Serialize any data to a binary representation in a sequence of bytes.
  • Crypto.sha512: bytes -> bytes. Computes the cryptographic hash of a byte array with the cryptographic Sha512 function. One can choose to hash with the Crypto.blake2b or the Crypto.sha256 functions instead. The resulting hash is then checked against the stored hash.

If the passed in string has the same hash as the stored data, then it has to be a call by the audience to verify the data. We then check if the caller has transferred less than 1 tez during the call [if Current.amount() < 1tz then]:

  • if so, the call would fail with the error “Not enough money, queries cost 1 tez.”
  • if not, the output is the pair of an empty operation list and the storage unaltered.

If the passed in string does not have the same hash as the stored data, there are only two legitimate possibilities:

  1. A member of the audience has called the contract and has sent compromised data.
  2. The publisher has called the contract to update the data.

When the hashes don’t match, the contract checks if the caller of the contract is not the publisher [if Current.sender () <> storage.publisher then]:

  • if so, the call would fail with the error “Cannot authenticate.” The contract can neither authenticate the message nor the publisher’s identity (in the case of a third party attempting to update the data).
  • if not, the output is the pair of an empty operation list and the storage with the data updated, after it has been packed into bytes and hashed.

Let’s see an example of the contract in action!

To deploy the contract, the publisher first needs to hash the data in SHA512. Using the tezos client, I hashed the data “init”:

tezos-client hash data '"init"' of type string

The output shows various hashes:

Raw packed data: 0x050100000004696e6974
Hash: exprvHLcPB4RNz81dS3NxJeYW32Bx7JLYgQRk6DsDkpTTPJ6VBiCGV
Raw Blake2b hash: 0xe8a6320c82d7e05a4ef7b2ee9f4f4258aa329975ff13088821b1a0b4e4aff217
Raw Sha256 hash: 0x6da18735884f64ab1a57a85c04369c70d684c77d23778691992e6a110feee349
Raw Sha512 hash: 0xda83de3bdf464c9fd2522bc248ab1370994dc0b43bc237b6301462edf8701aca5da576a78450082f427fd77e9bfdddb036df243892269f29b98c2357983efa2b
Gas remaining: 399918 units remaining

The Raw Sha512 hash is the one we'll pass on to call the contract.

Next, compile data_publisher_hash.liq to data_publisher_hash.tz with Liquidity:

liquidity [path to]/data_publsher_hash.liq

Then, I ran the following command to deploy the contract with the tezos-client:

  • I named the contract publisher_hash
  • adam (tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx) is the manager
  • recall the --init option is the input for the initial storage. In this example the input is '(Pair "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" 0xda83de3bdf464c9fd2522bc248ab1370994dc0b43bc237b6301462edf8701aca5da576a78450082f427fd77e9bfdddb036df243892269f29b98c2357983efa2b)'. In Michelson we input a record with two fields as a pair. The first item of the pair is the address of the publisher (adam with address "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"). The second item is the data to be published in hashed SHA512 form.
tezos-client originate contract publisher_hash for adam transferring 1 from adam running ~/CryptiumLabs/smarter-contracts/liquidity/examples/data_publisher/data_publisher_hash.tz --init '(Pair "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx" 0xda83de3bdf464c9fd2522bc248ab1370994dc0b43bc237b6301462edf8701aca5da576a78450082f427fd77e9bfdddb036df243892269f29b98c2357983efa2b)' --burn-cap 0.874Node is bootstrapped, ready for injecting operations.
Estimated gas: 23497 units (will add 100 for safety)
Estimated storage: 874 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'ooAF5bSKtAB7Q9d2weemVWMSGUkA9MFhy6AdvdJn36opqJhNqT7'
Waiting for the operation to be included...
Operation found in block: BMNUn551YxaeoSxTzHyvgsE5DCV2XqqevSWG6ivczHy7zAaaKkx (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
Fee to the baker: ꜩ0.003246
Expected counter: 1
Gas limit: 23597
Storage limit: 894 bytes
Balance updates:
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ........... -ꜩ0.003246
fees(tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx,0) ... +ꜩ0.003246
Origination:
From: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
For: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
Credit: ꜩ1
Script:
{ parameter string ;
storage (pair :storage (address %publisher) (bytes %data)) ;
code { DUP ;
DIP { CDR @storage_slash_1 } ;
CAR @param_slash_2 ;
{ DIP { DUP @storage } ; SWAP } ;
CDR %data ;
{ DIP { DUP @param } ; SWAP } ;
PACK ;
SHA512 ;
COMPARE ;
EQ ;
IF { PUSH mutez 1000000 ;
AMOUNT ;
COMPARE ;
LT ;
IF { PUSH string "Not enough money, queries cost 1 tez." ; FAILWITH }
{ { DIP { DUP @storage } ; SWAP } ; NIL operation ; PAIR } }
{ { DIP { DUP @storage } ; SWAP } ;
CAR %publisher ;
SENDER ;
COMPARE ;
NEQ ;
IF { PUSH string "Cannot authenticate." ; FAILWITH }
{ { DIP { DUP @storage } ; SWAP } ;
CAR %publisher ;
{ DIP { DUP @param } ; SWAP } ;
PACK ;
SHA512 ;
SWAP ;
PAIR %publisher %data ;
NIL operation ;
PAIR } } ;
DIP { DROP ; DROP } } }
Initial storage:
(Pair "tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx"
0xda83de3bdf464c9fd2522bc248ab1370994dc0b43bc237b6301462edf8701aca5da576a78450082f427fd77e9bfdddb036df243892269f29b98c2357983efa2b)
No delegate for this contract
This origination was successfully applied
Originated contracts:
KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi
Storage size: 617 bytes
Paid storage size diff: 617 bytes
Consumed gas: 23497
Balance updates:
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ0.617
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ0.257
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ... -ꜩ1
KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi ... +ꜩ1

New contract KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi originated.
The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for ooAF5bSKtAB7Q9d2weemVWMSGUkA9MFhy6AdvdJn36opqJhNqT7 to be included --confirmations 30 --branch BLapjzdvoZf413Srw4Docs5AitsVFESubverSZpwGPYKraYrhRR
and/or an external block explorer.
Contract memorized as publisher_hash.

To update the data, adam has to call the contract, and the storage is updated:

tezos-client transfer 0 from adam to publisher_hash --arg '"update"' --burn-cap 0.002Node is bootstrapped, ready for injecting operations.
Estimated gas: 22394 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'ooWKCUMP8JyvHPVM4gpBUvVbHew3t4tLcEeaLjAtnJFj4bvVLib'
Waiting for the operation to be included...
Operation found in block: BMU9yD8xD5NhQrD9o8LFVzFARa3HzfD8SCbnvnzxse8wVChX6nL (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
Fee to the baker: ꜩ0.002515
Expected counter: 2
Gas limit: 22494
Storage limit: 0 bytes
Balance updates:
tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx ........... -ꜩ0.002515
fees(tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx,0) ... +ꜩ0.002515
Transaction:
Amount: ꜩ0
From: tz1KqTpEZ7Yob7QbPE4Hy4Wo8fHG8LhKxZSx
To: KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi
Parameter: "update"
This transaction was successfully applied
Updated storage:
(Pair 0x000002298c03ed7d454a101eb7022bc95f7e5f41ac78
0x44e8c82b87b7259061cf6a9b73975b68f3aae1c0d14921873f04394b971cacd52cd4d9fde8844f3c519278f3e9e64d9e3c9ea0a588a82573fae6897f7d0c7156)
Storage size: 617 bytes
Consumed gas: 22394

The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for ooWKCUMP8JyvHPVM4gpBUvVbHew3t4tLcEeaLjAtnJFj4bvVLib to be included --confirmations 30 --branch BMNUn551YxaeoSxTzHyvgsE5DCV2XqqevSWG6ivczHy7zAaaKkx
and/or an external block explorer.

When bob calls the contract to update the storage, the call fails because bob isn’t the specified publisher:

tezos-client transfer 0 from bob to publisher_hash --arg '"Bob breaks this!"' --burn-cap 0.002

The call fails even when bob pays 1 tez to the contract:

tezos-client transfer 1 from bob to publisher_hash --arg '"Bob breaks this!"' --burn-cap 0.002Node is bootstrapped, ready for injecting operations.
This simulation failed:
Manager signed operations:
From: tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN
Fee to the baker: ꜩ0
Expected counter: 1
Gas limit: 400000
Storage limit: 60000 bytes
Transaction:
Amount: ꜩ1
From: tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN
To: KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi
Parameter: "Bob breaks this!"
This operation FAILED.

Runtime error in contract KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi:
01: { parameter string ;
......
34: DIP { DROP ; DROP } } }
At line 24 characters 56 to 64,
script reached FAILWITH instruction
with "Cannot authenticate."
Fatal error:
transfer simulation failed

Anyone can call the contract to verify the data one has. When the amount is no less than 1tz, the call is successful:

tezos-client transfer 1 from bob to publisher_hash --arg '"update"' --burn-cap 0.002Node is bootstrapped, ready for injecting operations.
Estimated gas: 22047 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'ooqZKrAyreTiXNnUKEr7G49RizY2CHTuUTTWruSFKhL8oaodnLV'
Waiting for the operation to be included...
Operation found in block: BLJ4U94tMkHXTCnawFEhDrk6Ac7DoCU8EJFk3NekzU2CTpmbV9o (pass: 3, offset: 0)
This sequence of operations was run:
Manager signed operations:
From: tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN
Fee to the baker: ꜩ0.002482
Expected counter: 1
Gas limit: 22147
Storage limit: 0 bytes
Balance updates:
tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN ........... -ꜩ0.002482
fees(tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN,0) ... +ꜩ0.002482
Transaction:
Amount: ꜩ1
From: tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN
To: KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi
Parameter: "update"
This transaction was successfully applied
Updated storage:
(Pair 0x000002298c03ed7d454a101eb7022bc95f7e5f41ac78
0x44e8c82b87b7259061cf6a9b73975b68f3aae1c0d14921873f04394b971cacd52cd4d9fde8844f3c519278f3e9e64d9e3c9ea0a588a82573fae6897f7d0c7156)
Storage size: 617 bytes
Consumed gas: 22047
Balance updates:
tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN ... -ꜩ1
KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi ... +ꜩ1

The operation has only been included 0 blocks ago.
We recommend to wait more.
Use command
tezos-client wait for ooqZKrAyreTiXNnUKEr7G49RizY2CHTuUTTWruSFKhL8oaodnLV to be included --confirmations 30 --branch BMU9yD8xD5NhQrD9o8LFVzFARa3HzfD8SCbnvnzxse8wVChX6nL
and/or an external block explorer.

But not when bob doesn’t pay enough tez:

tezos-client transfer 0 from bootstrap2 to publisher_hash --arg '"update"' --burn-cap 0.002Node is bootstrapped, ready for injecting operations.
This simulation failed:
Manager signed operations:
From: tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN
Fee to the baker: ꜩ0
Expected counter: 2
Gas limit: 400000
Storage limit: 60000 bytes
Transaction:
Amount: ꜩ0
From: tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN
To: KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi
Parameter: "update"
This operation FAILED.

Runtime error in contract KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi:
01: { parameter string ;
......
34: DIP { DROP ; DROP } } }
At line 17 characters 73 to 81,
script reached FAILWITH instruction
with "Not enough money, queries cost 1 tez."
Fatal error:
transfer simulation failed

When bob passes in compromised data, the contract returns “Cannot authenticate” (in fact, whether he had paid or not!):

tezos-client transfer 1 from bootstrap2 to publisher_hash --arg '"bob breaks this!"' --burn-cap 0.002Node is bootstrapped, ready for injecting operations.
This simulation failed:
Manager signed operations:
From: tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN
Fee to the baker: ꜩ0
Expected counter: 2
Gas limit: 400000
Storage limit: 60000 bytes
Transaction:
Amount: ꜩ1
From: tz1gjaF81ZRRvdzjobyfVNsAeSC6PScjfQwN
To: KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi
Parameter: "bob breaks this!"
This operation FAILED.

Runtime error in contract KT1BjNdkJqq2qNKiF3vHRRYzJJo3ejLwMrPi:
01: { parameter string ;
......
34: DIP { DROP ; DROP } } }
At line 24 characters 56 to 64,
script reached FAILWITH instruction
with "Cannot authenticate."
Fatal error:
transfer simulation failed

Woohoo! You’ve written a publisher contract that can securely publish data of arbitrary sizes!

In the next tutorial, we’ll write a contract that enables you to launch a token system on Tezos. Stay tuned!

Blockchain protocol/smart contract engineer @CryptiumLabs. thealmarty.com, twitter: @MartyStumpf.