Skip to main content

RedStone Oracles

How RedStone oracles work with TON

RedStone oracles use an alternative design of providing oracle data to smart contracts. Instead of constantly persisting data on the contract's storage (by data providers), the information is brought on-chain only when needed (by end users). Until that moment data remains in the decentralized cache layer, which is powered by RedStone light cache gateways and streams data broadcasting protocol. Data is transferred to the contract by end users, who should attach signed data packages to their function invocations. The information integrity is verified on-chain through signature checking.

To learn more about RedStone oracles design go to the RedStone docs

Smart Contracts

price_manager.fc

initial data

As mentioned above, the data packages transferred to the contract are being verified by signature checking. To be counted to achieve the signer_count_threshold, the signer signing the passed data should be one of the signers passed in the initial data. There is also needed signer_count_threshold to be passed.

Due to the architecture of TON contracts, the initial data must convene with the contract's storage structure, which is constructed as below:

  begin_cell()
.store_uint(signer_count_threshold, 8) /// number as passed below
.store_uint(timestamp, TIMESTAMP_BITS) /// initially 0 representing the epoch 0
.store_ref(signers) /// serialized tuple of values passed below
.end_cell();

The value of signers should be passed as a serialized tuple of ints. See tuple.

In the function parameters below, each feed_id is a string encoded to int which means, that's a value consisting of hex-values of the particular letters in the string. For example: 'ETH' as an int is 0x455448 in hex or 4543560 in decimal, as 256*256*ord('E')+256*ord('T')+ord('H').

You can use: feed_id=hexlify(toUtf8Bytes(feed_string)) to convert particular values or the endpoint

The value of feed_ids should be passed as a serialized tuple of ints.

The value payload is packed from an array of bytes representing the serialized RedStone payload. See TON RedStone payload packing section below, as well as the file constants.fc, containing all needed int-length constants.

get_prices

(cell) get_prices_v2(cell data_feed_ids, cell payload) method_id;

The function process on-chain the payload passed as an argument and returns a cell of aggregated values of each feed passed as an identifier inside feed_ids.

Due to HTTP GET method length limitation in TON API v4, the function is written for TON API v2.

That are just a method_id functions - they don't modify the contract's storage and don't consume TONs.

OP_REDSTONE_WRITE_PRICES

Regardless of the on-fly processing, there also exists a method for processing the payload on-chain, but saving/writing the aggregated values to the contract's storage. The values persist in the contract's storage and then can be read by using read_prices function. The timestamp of data last saved/written to the contract is able to read by using the read_timestamp function.

The method must be invoked as a TON internal message. The arguments of the message are:

  • an int representing RedStone_Write_Prices name hashed by keccak256 as defined in constants.ts
  • a cell - ref representing the data_feed_ids as a serialized tuple of ints.\
  • a cell - ref representing the packed RedStone payload
    int op = in_msg_body~load_uint(OP_NUMBER_BITS);

if (op == OP_REDSTONE_WRITE_PRICES) {
cell data_feeds_cell = in_msg_body~load_ref();
cell payload_cell = in_msg_body~load_ref();

// ...
}

That's an internal message - it consumes GAS and modifies the contract's storage, so must be paid by TONs.

See how it works on: https://ton-showroom.redstone.finance/

read_prices

(tuple) read_prices(tuple data_feed_ids) method_id;

The function reads the values persisting in the contract's storage and returns a tuple corresponding to the passed feed_ids. The function doesn't modify the storage and can read only aggregated values of the feed_ids saved by using write_prices function.

That's just a method_id function - it doesn't modify the contract's storage and don't consume TONs.

read_timestamp

(int) read_timestamp() method_id;

Returns the timestamp of data last saved/written to the contract's storage by using OP_REDSTONE_WRITE_PRICES message.

That's just a method_id function - it doesn't modify the contract's storage and don't consume TONs.

price_feed.fc

Due to the architecture of TON contracts, the initial data must convene with the contract's storage structure, which is constructed as below:

beginCell()
.storeUint(BigInt(hexlify(toUtf8Bytes(this.feedId))), consts.DATA_FEED_ID_BS * 8)
.storeAddress(Address.parse(this.managerAddress))
.storeUint(0, consts.DEFAULT_NUM_VALUE_BS * 8) /// initially 0 representing the epoch 0
.storeUint(0, consts.TIMESTAMP_BS * 8)
.endCell();

To define the initial (storage) data for the Price feed contract, use the predefined class PriceFeedInitData.ts.

OP_REDSTONE_FETCH_DATA

Regardless of reading the values persisting in the contract's from outside the network, there is a possibility for fetching the value stored in the contract for a feed_id on-chain directly. There must be invoked an internal message OP_REDSTONE_FETCH_DATA. The arguments of the message are:

  • an int representing RedStone_Fetch_Data name hashed by keccak256 as defined in constants.ts
  • an int representing the feed_id value.
  • a slice representing the initial_sender of the message, to allow they carried the remaining transaction balance when the returning transaction goes.
    int op = in_msg_body~load_uint(OP_NUMBER_BITS);

if (op == OP_REDSTONE_FETCH_DATA) {
int feed_id = in_msg_body~load_uint(DATA_FEED_ID_BITS);
cell initial_payload = in_msg_body~load_ref();

// ...
}

The returning message OP_REDSTONE_DATA_FETCHED message is sent to the sender, containing the value and the timestamp of the value has saved. The message can be then fetched in the sender and processed or saved in the sender's storage. The initial payload's ref (initial_payload) is added as a ref - containing for example the first message's sender, to allow they carry the remaining transaction balance.

begin_cell()
.store_uint(value, MAX_VALUE_SIZE_BITS)
.store_uint(timestamp, TIMESTAMP_BITS)
.store_ref(initial_payload)
.end_cell()

That's an internal message - it consumes GAS and modifies the contract's storage, so must be paid by TONs.

get_price_and_timestamp

(int, int) get_price_and_timestamp() method_id;

Returns the value and timestamp of the last saved/written data to the adapter's storage by sending OP_REDSTONE_FETCH_DATA message and fetching the returned value of the OP_REDSTONE_DATA_FETCHED message.

That's just a method_id function - it doesn't modify the contract's storage and don't consume TONs.

single_feed_man.fc

initial data

Similar to the prices and price_feed initial data. Due to the architecture of TON contracts, the initial data must convene with the contract's storage structure, which is constructed as below:

beginCell()
.storeUint(BigInt(hexlify(toUtf8Bytes(this.feedId))), consts.DATA_FEED_ID_BS * 8)
.storeUint(this.signerCountThreshold, SIGNER_COUNT_THRESHOLD_BITS)
.storeUint(0, consts.DEFAULT_NUM_VALUE_BS * 8)
.storeUint(0, consts.TIMESTAMP_BS * 8)
.storeRef(serializeTuple(createTupleItems(this.signers)))
.endCell();

To define the initial (storage) data for the Prices contract, use the predefined class SingleFeedManInitData.ts.

A contract like price_manager, but supporting the single feed only, to omit the communication needs between feed and manager contracts.

get_price

(int, int) get_price(cell payload) method_id;

Similar to get_prices, but omitting the first (data_feed_ids) argument as have it configured during the initialization. Returns also the min timestamp of the passed data packages.

read_price_and_timestamp

(int, int) read_price_and_timestamp() method_id;

Works as the get_price_and_timestamp function.

OP_REDSTONE_WRITE_PRICE

Similar to OP_REDSTONE_WRITE_PRICES, but omitting the first (data_feed_ids) cell-ref as have it configured during the initialization.

    int op = in_msg_body~load_uint(OP_NUMBER_BITS);

if (op == OP_REDSTONE_WRITE_PRICE) {
cell payload_cell = in_msg_body~load_ref();

// ...
}

sample_consumer.fc

A sample consumer for data stored in the price_feed. Works also with single_feed_man. The price_feed to be called needs to be passed.

initial data

Similar to the price_feed initial data. Due to the architecture of TON contracts, the initial data must convene with the contract's storage structure, which is constructed as below:

beginCell()
.storeAddress(Address.parse(this.feedAddress))
.endCell();

To define the initial (storage) data for the Prices contract, use the predefined class SampleConsumerInitData.ts.

The contract calls the single feed.

OP_REDSTONE_READ_DATA

There is a possibility for fetching the value stored in the contract for a feed_id on-chain directly. There must be invoked an internal message OP_REDSTONE_READ_DATA. The arguments of the message are:

  • a slice representing the initial_sender of the message, to allow they carried the remaining transaction balance when the returning transaction goes.
    int op = in_msg_body~load_uint(OP_NUMBER_BITS);

if (op == OP_REDSTONE_READ_DATA) {
cell initial_payload = in_msg_body~load_ref();

// ...
}

The returning message OP_REDSTONE_DATA_READ message is sent to the sender, containing the feed_id, value and the timestamp of the value has saved. The message can be then fetched in the sender and processed or saved in the sender's storage. The initial payload's ref (initial_payload) is added as a ref - containing for example the first message's sender, to allow they carry the remaining transaction balance.

begin_cell()
.store_uint(value, MAX_VALUE_SIZE_BITS)
.store_uint(timestamp, TIMESTAMP_BITS)
.store_ref(initial_payload)
.end_cell()

That's an internal message - it consumes GAS and modifies the contract's storage, so must be paid by TONs.

TON RedStone Payload packing

Due to limitations of the Bag-size in TON see, the RedStone payload data - represented as a hex string - needed to be passed to a contract in a more complex way.

Having the RedStone payload as defined here, the data should be passed as a Cell built as follows.

  1. The main payload cell consists of:

    1. the metadata in the data-level bits consisting of the parts as on the image:

    payload-metadata.png

    1. a ref containing a udict indexed by consecutive natural numbers (beginning from 0) containing the list of data_package cells.
  2. Each data-package cell consists of:

    1. the data package's signature in the data-level bits:

    payload-metadata.png

    1. one ref to a cell containing the data of the rest of the data package on its data-level:

    payload-metadata.png

Current implementation limitations

  • The RedStone payload must be fetched by explicitly defining data feeds, which leads to one data point belonging to one data package.
  • The unsigned metadata size must not be exceeding 127 - (2 + 3 + 9) = 113 bytes.

Helper

The createPayloadCell method in the create-payload-cell.ts file checks the limitations and prepares the data to be sent to the contract as described above.

Sample serialization

The image below contains data for 2 feeds times 2 unique signers: payload-metadata.png

Possible transaction failures

  • The number of signers recovered from the signatures matched with addresses passed in the initializer must be greater or equal that the signer_count_threshold in the constructor, for each feed.
    • Otherwise, it panics then with the 300 error, increased by the first index of the passed feed which has broken the validation.
  • The timestamp of data-packages must be not older than 15 minutes in relation to the block_timestamp.
    • Otherwise, it panics then with the 200 error, increased by the first index of the payload's data package which has broken the validation, increased additionally by 50 if the package's timestamp is too future.
  • The internal messages consume gas and must be paid by TONs. The data are available on the contract just after the transaction successes.
  • The other error codes are defined here

See Also