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.

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!

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:

the parameter of the contract, which I named param, of type string. 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.

. 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.

Data query

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.

Update data

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

A member of the audience has called the contract and has sent compromised data. 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!

Deploy

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

(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.874 Node 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.

Call to update data

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.002 Node 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.002 Node 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

Call to verify data

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.002 Node 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.002 Node 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.002 Node 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!