Spaces:
Runtime error
Runtime error
| sidebar_position: 3 | |
| # Tutorial 3: Oracle | |
| ## Overview | |
| In this tutorial, we will go over how to build a smart contract that consumes off-chain data from an oracle. Specifically, we will implement a smart contract that lets two players bet on the price of BSV at some point in the future. It retrieves prices from an oracle. | |
| ### What is an Oracle? | |
| A blockchain oracle is a third-party service or agent that provides external data to a blockchain network. It is a bridge between the blockchain and the external world, enabling smart contracts to access, verify, and incorporate data from outside the blockchain. This allows smart contracts to execute based on real-world events and conditions, enhancing their utility and functionality. | |
|  | |
| [Credit: bitnovo](https://blog.bitnovo.com/en/what-is-a-blockchain-oracle/) | |
| The data supplied by oracles can include various types of information, such as stock prices, weather data, election results, and sports scores. | |
| ### Rabin Signatures | |
| A digital signature is required to verify the authenticity and integrity of arbitrary data provided by known oracles in a smart contract. Instead of ECDSA used in Bitcoin, we use an alternative digital signature algorithm called [Rabin signatures](https://en.wikipedia.org/wiki/Rabin_signature_algorithm). This is because Rabin signature verification is orders of magnitude cheaper than ECDSA. | |
| We have implemented [Rabin signature](https://github.com/sCrypt-Inc/scrypt-ts-lib/blob/master/src/rabinSignature.ts) as part of the standard libraries [`scrypt-ts-lib`](https://www.npmjs.com/package/scrypt-ts-lib), which can be imported and used directly. | |
| ## Contract Properties | |
| Our contract will take signed pricing data from the [WitnessOnChain oracle](https://witnessonchain.com). Depending if the price target is reached or not, it will pay out a reward to one of the two players. | |
| There are quite a few properties which our price betting smart contract will require: | |
| ```ts | |
| // Price target that needs to be reached. | |
| @prop() | |
| targetPrice: bigint | |
| // Symbol of the pair, e.g. "BSV_USDC" | |
| @prop() | |
| symbol: ByteString | |
| // Timestamp window in which the price target needs to be reached. | |
| @prop() | |
| timestampFrom: bigint | |
| @prop() | |
| timestampTo: bigint | |
| // Oracles Rabin public key. | |
| @prop() | |
| oraclePubKey: RabinPubKey | |
| // Addresses of both players. | |
| @prop() | |
| alicePkh: PubKeyHash | |
| @prop() | |
| bobPkh: PubKeyHash | |
| ``` | |
| Notice that the type `RabinPubKey`, which represents a Rabin public key, is not a standard type. You can import it the following way: | |
| ```ts | |
| import { RabinPubKey } from 'scrypt-ts-lib' | |
| ``` | |
| ## Public Method - `unlock` | |
| The contract will have only a single public method, namely `unlock`. As parameters, it will take the oracles signature, the signed message from the oracle, and a signature of the winner, who can unlock the funds: | |
| ```ts | |
| @method() | |
| public unlock(msg: ByteString, sig: RabinSig, winnerSig: Sig) { | |
| // Verify oracle signature. | |
| assert( | |
| RabinVerifierWOC.verifySig(msg, sig, this.oraclePubKey), | |
| 'Oracle sig verify failed.' | |
| ) | |
| // Decode data. | |
| const exchangeRate = PriceBet.parseExchangeRate(msg) | |
| // Validate data. | |
| assert( | |
| exchangeRate.timestamp >= this.timestampFrom, | |
| 'Timestamp too early.' | |
| ) | |
| assert( | |
| exchangeRate.timestamp <= this.timestampTo, | |
| 'Timestamp too late.' | |
| ) | |
| assert(exchangeRate.symbol == this.symbol, 'Wrong symbol.') | |
| // Decide winner and check their signature. | |
| const winner = | |
| exchangeRate.price >= this.targetPrice | |
| ? this.alicePubKey | |
| : this.bobPubKey | |
| assert(this.checkSig(winnerSig, winner)) | |
| } | |
| ``` | |
| Let's walk through each part. | |
| First, we verify that the passed signature is correct. For that we use the `RabinVerifierWOC` library from the [`scrypt-ts-lib`](https://www.npmjs.com/package/scrypt-ts-lib) package | |
| ```ts | |
| import { RabinPubKey, RabinSig, RabinVerifierWoc } from 'scrypt-ts-lib' | |
| ``` | |
| Now, we can call the `verifySig` method of the verification library: | |
| ```ts | |
| // Verify oracle signature. | |
| assert( | |
| RabinVerifierWOC.verifySig(msg, sig, this.oraclePubKey), | |
| 'Oracle sig verify failed.' | |
| ) | |
| ``` | |
| The verification method requires the message signed by the oracle, the oracles signature for the message, and the oracle's public key, which we already set via the constructor. | |
| Next, we need to parse information from the chunk of data that is the signed message and assert on it. For a granular description of the message format check out the `"Exchange Rate"` section in the [WitnessOnChain API docs](https://witnessonchain.com). | |
| We need to implement the static method `parseExchangeRate` as follows: | |
| ```ts | |
| // Parses signed message from the oracle. | |
| @method() | |
| static parseExchangeRate(msg: ByteString): ExchangeRate { | |
| // 4 bytes timestamp (LE) + 8 bytes rate (LE) + 1 byte decimal + 16 bytes symbol | |
| return { | |
| timestamp: Utils.fromLEUnsigned(slice(msg, 0n, 4n)), | |
| price: Utils.fromLEUnsigned(slice(msg, 4n, 12n)), | |
| symbol: slice(msg, 13n, 29n), | |
| } | |
| } | |
| ``` | |
| We parse out the following data: | |
| - `timestamp` - The time at which this exchange rate is present. | |
| - `price` - The exchange rate encoded as an integer -> (priceFloat * (10^decimal)). | |
| - `symbol` - The symbol of the token pair, e.g. `BSV_USDC`. | |
| Finally, we wrap the parsed values in a custom type, named `ExchangeRate` and return it. Here's the definition of the type: | |
| ```ts | |
| type ExchangeRate = { | |
| timestamp: bigint | |
| price: bigint | |
| symbol: ByteString | |
| } | |
| ``` | |
| Now we can validate the data. First, we check if the timestamp of the exchange rate is within our specified range that we bet on: | |
| ```ts | |
| assert( | |
| exchangeRate.timestamp >= this.timestampFrom, | |
| 'Timestamp too early.' | |
| ) | |
| assert( | |
| exchangeRate.timestamp <= this.timestampTo, | |
| 'Timestamp too late.' | |
| ) | |
| ``` | |
| Additionally, we check if the exchange rate is actually for the correct token pair: | |
| ```ts | |
| assert(exchangeRate.symbol == this.symbol, 'Wrong symbol.') | |
| ``` | |
| Lastly, upon having all the necessary information, we can choose the winner and check their signature: | |
| ```ts | |
| const winner = | |
| exchangeRate.price >= this.targetPrice | |
| ? this.alicePubKey | |
| : this.bobPubKey | |
| assert(this.checkSig(winnerSig, winner)) | |
| ``` | |
| As we can see, if the target price is reached, only Alice is able to unlock the funds, and if not, then only Bob is able to do so. | |
| ## Conclusion | |
| Congratulations! You have completed the oracle tutorial! | |
| The full code along with [tests](https://github.com/sCrypt-Inc/boilerplate/blob/master/tests/local/priceBet.test.ts) can be found in sCrypt's [boilerplate repository](https://github.com/sCrypt-Inc/boilerplate/blob/master/src/contracts/priceBet.ts). | |