Oracle

Introduction

Smart contracts that require blockchain-external realtime data or events require a trusted oracle, i.e. a trusted smart contract that provides the data on-chain.

What is it useful for?

  • An oracle that provides XTZ/USD prices could allow users to deposit funds that are immediately converted to some on-chain asset and represented as fiat.
  • An oracle that provides weather data could allow a tsunami insurance contract: users purchase coverage from the contract, which pays out when there's a sufficiently severe tsunami in their area
  • A combination of different oracles could be used to provide consensus, for example, where only results published by a majority of the oracles are considered valid.

Setup

The CLI

❯❯❯ lorentz-contract-oracle Oracle --help
Usage: lorentz-contract-oracle Oracle COMMAND
Oracle contract CLI interface
Available options:
-h,--help Show this help text
Available commands:
print Dump the Oracle contract in form of Michelson code
print-timestamped Dump the Timestamped Oracle contract in form of
Michelson code
init Initial storage for the Oracle contract
get-value get value
update-value update value
update-admin update admin

Originating the contract

Printing the contract

The print command takes a single argument: valueType, the type of the value provided by the oracle.

For example, if nat's are provided:

❯❯❯ lorentz-contract-oracle Oracle print --valueType "nat"
parameter (or (pair unit
(contract nat))
(or nat
address));
storage (pair nat
address);
code { DUP;
CAR;
DIP { CDR };
IF_LEFT { DUP;
CAR;
DIP { CDR };
DIP { DIP { DUP };
SWAP };
PAIR;
CDR;
CAR;
DIP { AMOUNT };
TRANSFER_TOKENS;
NIL operation;
SWAP;
CONS;
PAIR }
{ IF_LEFT { DIP { DUP;
CAR;
DIP { CDR } };
DIP { DROP;
DUP;
DIP { SENDER;
COMPARE;
EQ;
IF { }
{ PUSH string "only admin may update";
FAILWITH } } };
PAIR;
NIL operation;
PAIR }
{ DIP { DUP;
CAR;
DIP { CDR };
DIP { SENDER;
COMPARE;
EQ;
IF { }
{ PUSH string "only admin may update";
FAILWITH } } };
SWAP;
PAIR;
NIL operation;
PAIR } } };

Initial storage

❯❯❯ lorentz-contract-oracle Oracle init --help
Usage: lorentz-contract-oracle Oracle init --initialValueType Michelson Type
--initialValue Michelson Value
--admin ADDRESS
Initial storage for the Oracle contract
Available options:
-h,--help Show this help text
--initialValueType Michelson Type
The Michelson Type of initialValue
--initialValue Michelson Value
The Michelson Value: initialValue
--admin ADDRESS Address of the admin.
-h,--help Show this help text

Since we're using nat, initialValueType is nat and initialValue can be 0:

❯❯❯ lorentz-contract-oracle Oracle init \
--initialValueType "nat" \
--initialValue 3 \
--admin $ALICE_ADDRESS
Pair 3 "tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr"

Running the origination

NOTE: to use the annotated (timestamped) version, e.g. for nat:

❯❯❯ tezos-client --wait none originate contract NatOracle \
transferring 0 from $ALICE_ADDRESS running \
"$(cat nat_oracle.tz)" \
--init "$(lorentz-contract-oracle Oracle init \
--initialValueType "pair timestamp nat" --initialValue 'Pair "2019-12-10T17:43:53Z" 26871' \
--admin $ALICE_ADDRESS)" --burn-cap 0.859 --force

NOTE: the nat_oracle.tz contract may be found here

Otherwise:

❯❯❯ tezos-client --wait none originate contract NatOracle \
transferring 0 from $ALICE_ADDRESS running \
"$(lorentz-contract-oracle Oracle print --valueType "nat")" \
--init "$(lorentz-contract-oracle Oracle init \
--initialValueType "nat" --initialValue 3 \
--admin $ALICE_ADDRESS)" --burn-cap 0.612
Waiting for the node to be bootstrapped before injection...
Current head: BKvCNwrrSzp7 (timestamp: 2019-12-07T00:30:08-00:00, validation: 2019-12-07T00:30:46-00:00)
Node is bootstrapped, ready for injecting operations.
Estimated gas: 18987 units (will add 100 for safety)
Estimated storage: 612 bytes added (will add 20 for safety)
Operation successfully injected in the node.
Operation hash is 'onrarWezTWeZmBZxNM5edtYirv8ZdECxmjpvVRj8tA2JEaYNQJC'
NOT waiting for the operation to be included.
Use command
tezos-client wait for onrarWezTWeZmBZxNM5edtYirv8ZdECxmjpvVRj8tA2JEaYNQJC to be included --confirmations 30 --branch BKvCNwrrSzp7nav3iYQCC37pnsqDMMrFTWw3Z2nPNiHiRAT2LAh
and/or an external block explorer to make sure that it has been included.
This sequence of operations was run:
Manager signed operations:
From: tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr
Fee to the baker:0.002508
Expected counter: 30709
Gas limit: 19087
Storage limit: 632 bytes
Balance updates:
tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr ............ -0.002508
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,59) ... +0.002508
Origination:
From: tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr
Credit:0
Script:
{ parameter (or (pair unit (contract nat)) (or nat address)) ;
storage (pair nat address) ;
code { DUP ;
...
PAIR } } } }
Initial storage: (Pair 3 "tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr")
No delegate for this contract
This origination was successfully applied
Originated contracts:
KT1VTqmma3vCH9nkLL1Jakd6MiUwxwqieXDE
Storage size: 355 bytes
Paid storage size diff: 355 bytes
Consumed gas: 18987
Balance updates:
tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr ... -0.355
tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr ... -0.257
New contract KT1VTqmma3vCH9nkLL1Jakd6MiUwxwqieXDE originated.
Contract memorized as NatOracle.

Make an alias for the resulting address:

❯❯❯ ORACLE_ADDRESS="KT1VTqmma3vCH9nkLL1Jakd6MiUwxwqieXDE"

Getting a value

Preparing a view contract

Originate the contract:

❯❯❯ tezos-client --wait none originate contract nat_storage transferring 0 \
from $ALICE_ADDRESS running "$(lorentz-contract print --name NatStorageContract)" \
--init 0 --burn-cap 0.295

Make an alias for its address:

❯❯❯ NAT_STORAGE_ADDRESS="KT1JDLPVp9trdzNB7Xk1ETVXjaGDTMENn1vk"

See the FA1.2 Quickstart for more info.

Make the parameter

❯❯❯ lorentz-contract-oracle Oracle get-value --help
Usage: lorentz-contract-oracle Oracle get-value --callbackContract ADDRESS
get value
Available options:
-h,--help Show this help text
--callbackContract ADDRESS
Address of the callbackContract.
-h,--help Show this help text
❯❯❯ lorentz-contract-oracle Oracle get-value --callbackContract $NAT_STORAGE_ADDRESS
Left (Pair Unit "KT1JDLPVp9trdzNB7Xk1ETVXjaGDTMENn1vk")

Get the value

❯❯❯ tezos-client --wait none transfer 0 from $ALICE_ADDRESS to $ORACLE_ADDRESS \
--arg "$(lorentz-contract-oracle Oracle get-value \
--callbackContract $NAT_STORAGE_ADDRESS)" --burn-cap 0.000001
Waiting for the node to be bootstrapped before injection...
Current head: BMeYNKNuMDGK (timestamp: 2019-12-07T00:38:10-00:00, validation: 2019-12-07T00:39:04-00:00)
Node is bootstrapped, ready for injecting operations.
Estimated gas: 30424 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'onhfCx9f5khcQbmtermQpidTTKeJ9p6xLJN2C1sCtMcUxtxj2Jt'
NOT waiting for the operation to be included.
Use command
tezos-client wait for onhfCx9f5khcQbmtermQpidTTKeJ9p6xLJN2C1sCtMcUxtxj2Jt to be included --confirmations 30 --branch BMeYNKNuMDGKdyYH1VkUo3oPxYhKx812EEStVpD9wpSTCWtKU97
and/or an external block explorer to make sure that it has been included.
This sequence of operations was run:
Manager signed operations:
From: tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr
Fee to the baker:0.003356
Expected counter: 30711
Gas limit: 30524
Storage limit: 0 bytes
Balance updates:
tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr ............ -0.003356
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,59) ... +0.003356
Transaction:
Amount:0
From: tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr
To: KT1VTqmma3vCH9nkLL1Jakd6MiUwxwqieXDE
Parameter: (Left (Pair Unit "KT1JDLPVp9trdzNB7Xk1ETVXjaGDTMENn1vk"))
This transaction was successfully applied
Updated storage:
(Pair 3 0x00003b5d4596c032347b72fb51f688c45200d0cb50db)
Storage size: 355 bytes
Consumed gas: 19100
Internal operations:
Transaction:
Amount:0
From: KT1VTqmma3vCH9nkLL1Jakd6MiUwxwqieXDE
To: KT1JDLPVp9trdzNB7Xk1ETVXjaGDTMENn1vk
Parameter: 3
This transaction was successfully applied
Updated storage: 3
Storage size: 38 bytes
Consumed gas: 11324

Update the value

Make the parameter

❯❯❯ lorentz-contract-oracle Oracle update-value --help
Usage: lorentz-contract-oracle Oracle update-value --newValueType Michelson Type
--newValue Michelson Value
update value
Available options:
-h,--help Show this help text
--newValueType Michelson Type
The Michelson Type of newValue
--newValue Michelson Value
The Michelson Value: newValue
-h,--help Show this help text

Update the value

To update the value to 4:

❯❯❯ tezos-client --wait none transfer 0 from $ALICE_ADDRESS to $ORACLE_ADDRESS \
--arg "$(lorentz-contract-oracle Oracle update-value \
--newValueType "nat" --newValue 4)" --burn-cap 0.000001
Waiting for the node to be bootstrapped before injection...
Current head: BL1po3bXefnQ (timestamp: 2019-12-19T19:26:24-00:00, validation: 2019-12-19T19:26:37-00:00)
Node is bootstrapped, ready for injecting operations.
Estimated gas: 18322 units (will add 100 for safety)
Estimated storage: no bytes added
Operation successfully injected in the node.
Operation hash is 'opXsapCVyFJLMqjQDtP5qrfhPPXRFZcHBLkAMCEG57W4a2DkAk5'
NOT waiting for the operation to be included.
Use command
tezos-client wait for opXsapCVyFJLMqjQDtP5qrfhPPXRFZcHBLkAMCEG57W4a2DkAk5 to be included --confirmations 30 --branch BL1po3bXefnQJDnR3eUCCHK1eyUMZ2dT7SiMkcCagrmzE9ZPjaY
and/or an external block explorer to make sure that it has been included.
This sequence of operations was run:
Manager signed operations:
From: tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr
Fee to the baker:0.002105
Expected counter: 32530
Gas limit: 18422
Storage limit: 0 bytes
Balance updates:
tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr ............ -0.002105
fees(tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU,69) ... +0.002105
Transaction:
Amount:0
From: tz1R3vJ5TV8Y5pVj8dicBR23Zv8JArusDkYr
To: KT1VTqmma3vCH9nkLL1Jakd6MiUwxwqieXDE
Parameter: (Right (Left 4))
This transaction was successfully applied
Updated storage:
(Pair 4 0x00003b5d4596c032347b72fb51f688c45200d0cb50db)
Storage size: 355 bytes
Consumed gas: 18322

Setting up a server

While we have everything we need to originate and administer an oracle contract, we'd like to automatically push updates to the contract.

Below are two simple examples of how you can periodically fetch some data and automatically push it to the oracle contract.

Minimal Bash server

A minimal server, written in Bash, could consist of:

  • A Bash script update_value.sh that gets the NEW_VALUE and then calls the update-value entrypoint:
#!/bin/bash
# update_value.sh
NEW_VALUE="$(my_get_new_value_script)
tezos-client --wait none transfer 0 from $ALICE_ADDRESS to $ORACLE_ADDRESS \
--arg "$(lorentz-contract-oracle Oracle update-value \
--newValueType "nat" --newValue $NEW_VALUE)" --burn-cap 0.000001
  • A crontab job to run update_value.sh periodically (in the example it's daily at 5:03 pm):
03 05 * * * update_value.sh

Docker Flask server

There is an implementation of the above Bash server in Python, packaged as a Docker image, which:

  • Fetches the latest stock price from Alpha Vantage
  • Runs a "cron job" to push the update to the oracle contract with pytezos every 30 seconds
  • Serves a webpage for debugging

It does the same thing as the Bash script where my_get_new_value_script fetches the latest ticker price from Alpha Vantage.

You can find the project on Github here or view the one-file Python code here.

NOTE: This server only works with the annotated (timestamped) version of the oracle contract for natural numbers

To get the image from the DockerHub repo, run:

❯❯❯ docker pull tqtezos/oracle-stock-ticker:1.0

The server is configured using environment variables:

  • To generate the TEZOS_USER_KEY parameter, run: echo "$(base64 MY_KEY_FILE.json | tr -d '\n')", where MY_KEY_FILE.json is your Tezos faucet file (see here to get a testnet faucet file).
  • You can get a free ALPHA_VANTAGE_API_KEY from Alpha Vantage
  • Alpha Vantage's Search Endpoint may be used to find ALPHA_VANTAGE_TICKER_SYMBOL's
TEZOS_USER_KEY=".."
ORACLE_ADDRESS="KT1EGbAxguaWQFkV3Egb2Z1r933MWuEYyrJS"
ALPHA_VANTAGE_API_KEY=".."
ALPHA_VANTAGE_TICKER_SYMBOL="AAPL"
FLASK_APP="tq/oracles/ticker.py"

To run the Docker container:

❯❯❯ docker run -d -p 5000:5000 \
--env TEZOS_USER_KEY=".."
--env ORACLE_ADDRESS="KT1.." \
--env ALPHA_VANTAGE_API_KEY=".." \
--env ALPHA_VANTAGE_TICKER_SYMBOL="AAPL" \
--env FLASK_APP="tq/oracles/ticker.py" \
oracle-stock-ticker

Once it's up, it will serve the debug page and update the oracle contract roughly every 30 seconds.

You should be able to view the debug screen at localhost:5000, fetch the latest values using get-value, and use a block explorer to inspect the contract.