14

Building a Blockchain Oracle for Solana | by Donatas Kairys | Loadsys Solutions...

 2 years ago
source link: https://medium.com/loadsys-solutions/building-a-blockchain-oracle-for-solana-4556529ea841
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Building a Blockchain Oracle for Solana

A blockchain oracle is a third party service that provides data to a smart contract. An example of an oracle service would be stock prices, weather reports, temperatures, any data a smart contract needs to act and trust.

On Solana, a smart contract cannot not make external API calls and all data for the smart contract needs to be assembled by the client. It means the smart contract can’t trust the data that is passed to the contract unless it is stored on the chain and verified by digital signatures.

Solution

We will set up a simple Oracle solution with on-chained stored oracle data. The solution will include 2 programs: Provider and Oracle. The Provider program will act as our namespace and organization, Oracle — actual on chain data.

Repository Link.

This tutorial assumes you already have Solana CLI and Anchor configured.

Let’s dissect the code.

Accounts

To store data on the chain, we need to set up accounts.

Provider Account

programs/provider/src/lib.rs

#[account]
pub struct Provider {
//Provider name
pub name: String,// Authority for setting oracle value
pub authority: Pubkey,// Number of oracle data points
pub data_size: u32,// Bump seed
pub bump: u8,
}

We will use a program derived address to retrieve the account by generating a seed [name]. Due to this business decision, we will not be able to modify the name after the initialization, but it will simplify the oracle data retrieval, which we will discuss later.

The authority field holds the public key of the user who is authorized to make changes to the provider data and the oracles under it.

The data_size holds a number of how many different data points the oracle will store. It is required to calculate the space needed to store the oracle. In Solana, a program has to either cover the rent cost for storage or be exempt by holding a minimum amount of SOL in the account. Once space for an account is allocated, it can’t be changed.

Bump, also called a nonce, is a random number used to calculate a valid derived signature on the elliptic curve digital signature algorithm (ECDSA) when deriving an address from the program ID.

Oracle Account

programs/oracle/src/lib.rs

...
#[account]
pub struct Oracle {
// Provider Program ID
provider_program: Pubkey,// Oracle Name
name: String,// Oracle Data
data: Vec<OracleData>,// Bump seed
pub bump: u8,
}
...

The provider_program field holds a public key for the template program. We will use it to validate that the provider account passed with instructions actually belongs to the program. One of the attacks is to substitute accounts with invalid ones to gain access to an authorized resource.

The name will be used to identify and retrieve the data for a specific oracle. It will be used in the seed [provider.to_account_info().key.as_ref(), name_seed(&name)] when deriving an address from the program key.

The data field holds an array of data points we would like to store with the max number defined in the Provider object.

Instructions

Anchor framework very nicely abstracts a lot of code that we would otherwise have to write. With a few macros we define and validate accounts.

Initializing a Provider(namespace for oracles)

programs/provider/src/lib.rs

...
pub fn initialize(ctx: Context<Initialize>, name: String, size: u32, bump: u8) -> ProgramResult {
if !(size >= 0 as u32 && size <= MAX_DATA_SIZE) {
return Err(ErrorCode::ProviderInvalidSize.into());
}let provider = &mut ctx.accounts.provider;
provider.name = name;
provider.data_size = size;
provider.bump = bump;
provider.authority = *ctx.accounts.user.to_account_info().key;Ok(())
}...#[derive(Accounts)]
#[instruction(name: String, size: u32, bump: u8)]
pub struct Initialize<'info> {
#[account(init,
payer=user,
space=Provider::space(&name),
seeds=[name_seed(&name)],
bump=bump
)]
pub provider: Account<'info, Provider>,
#[account(mut)]
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[account(init, …)] macro configures a new account. For the account to be created, we need to tell the transaction processor who will pay for the transaction (payer=user, the user account will cover the fee), space (we calculate the space to allocate), an array of seeds and the bump to verify a program derived account.

Initializing an oracle instruction

programs/oracle/src/lib.rs

...
pub fn initialize(ctx: Context<Initialize>, name: String, data: Vec<OracleData>, bump: u8) -> ProgramResult {let oracle = &mut ctx.accounts.oracle;
oracle.name = name;
oracle.bump = bump;
oracle.data = data;
oracle.provider_program = *ctx.accounts.oracle_provider.to_account_info().owner;Ok(())
}pub fn update(ctx: Context<Update>, data: Vec<OracleData>) -> ProgramResult {let oracle = &mut ctx.accounts.oracle;
oracle.data = data;Ok(())
}
...#[derive(Accounts)]
#[instruction(name: String, data: Vec<OracleData>, bump: u8)]
pub struct Initialize<'info> {
#[account(init,
payer=user,
space=Oracle::space(&name, &oracle_provider.data_size),
seeds=[oracle_provider.to_account_info().key.as_ref(), name_seed(&name)],
bump=bump
)]
pub oracle: Account<'info, Oracle>,#[account(
//verify that the user is the oracle update authority
constraint = oracle_provider.authority == *user.key,
)]
pub oracle_provider: Account<'info, Provider>,// User initiating the transaction
#[account(mut)]
pub user: Signer<'info>,// System program
pub system_program: Program<'info, System>,
}

#[account(init, …)] macro configures a new data account. The seed defines an array for generating an oracle derived address.

The provider field is the parent account of the oracle. We are passing some constraints to validate the account:
constraint = provider.authority == *user.key makes sure the signer has the authority to add the oracle for the template. When updating, constraint = *provider.to_account_info().owner == oracle.provider_program checks if the provider belongs to the program. The provider program ID should be constant so the on-chain program can check against. We would like to make sure a malicious user cannot substitute an account and gain access to the oracle.

The user field is type Signer which automatically checks if the user is the signer of the transaction.

Conclusion

Please explore the concept in more detail [repo]. The tests provide a perfect location to see the program in action [tests]. The concept shows how to provide data to a smart contract that the transaction can trust.

There are many ways to improve the concept.

We could add active/inactive flags to the providers and oracles. We could add a Rust SDK for a simple way to retrieve the oracle such as Oracle:get(‘stocks.APPL/USD’, “value”) that would pull the proper provider and send the value back.

We can also add send events with emit!() macro and listen for the program logs.

We can also change the program to write the data in slots. That way, we can update multiple data points with a single transaction. Once one slot fills up, we start a new data account linked slot.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK