Using the Oracle on Solana

This article explains how the Umbrella Network's Chain Solana program works. It is a guide for developers who want to fetch first-class data values and verify Layer-2 data in Solana. We provide scripts as examples in our Reference Application.

Account structure

On Solana, we created accounts for each of the first-class data (FCD) and for each block where we store the Merkel root hash for L2 data verification. Since there is only one account for each FCD, their values are overwritten with every block. On the contrary, for every block in our Oracle we are creating a new account.

All the accounts in Solana have an address (public key). In our case, we make use of Partial Derived Addresses (PDA) which are public keys that don't rely on the elliptic curve, and as a consequence, they don't have any associated private key. We use this PDA for its hash-map property.

How to get data from an account

The first step is to compute the PDA for the account we are interested in. This is done with the findProgramAddress method which takes the ProgramId and a seed as parameters.

For Umbrella Network's Oracle, the seed could be a block ID or the key for the FCD:

const programId = "4SPgs3L7Ey9VyRuZwx4X3y86LSAZXP2Hhpz9Sps4v3iT";
const seed = LeafValueCoder.encode(blockId, '');
const [blockPda, bump] = await PublicKey.findProgramAddress(
    [seed], program.programId
);

📘

The LeafValueCoder, an object belonging to Umbrella Network;s SDK, serves to encode and decode common data types such as numbers or strings into data buffers which are most useful for being processed on the blockchain.

📘

PublicKey is an object that belongs to the @solana/web3.js interface and contains the findProgramAddress function, This function converts the seed and the program ID into a PDA.
These two parameters uniquely determine the blockPda.

After obtaining the blockPDA, we can proceed fetching the data using the program object:

let block = await program.account.authority.fetch(blockPda);

Retrieving values for First-class Data

As mentioned before, in order to retrieve the value for a FCD, the key of the required FCD should be used as seed for the FindProgramAddress function.

Follows an example for retrieving value for BTC-USD FCD:

const programId = "4SPgs3L7Ey9VyRuZwx4X3y86LSAZXP2Hhpz9Sps4v3iT";
let key = "BTC-USD";
const seed = LeafKeyCoder.encode(key);
const [blockPda, bump] = await PublicKey.findProgramAddress(
    [seed], program.programId
);
let fcd = await program.account.authority.fetch(blockPda);

Reading FCD from a Solana program

Umbrella Network provides a program with the data structures and functions to easily read FCD from another Solana program. These structures and functions can be found in the chain program on the this repo.

Below a code snippet depicting how to do it: (This code can be found in here)

#[program]
pub mod caller {
    use super::*;

    pub fn read_fcd(ctx: Context<FirstClassDataContext>) -> Result<()> {
        let fcd_account = &ctx.accounts.fcd;
        let key = fcd_account.key.clone();
        let value = fcd_account.value.clone();
        let timestamp = fcd_account.timestamp.clone();

        msg!("[caller] key: {:?}", key);
        msg!("[caller] value: {:?}", value);
        msg!("[caller] timestamp: {:?}", timestamp);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct FirstClassDataContext<'info> {
    pub fcd: Account<'info, FirstClassData>,
}

And this is how you can invoke the program using TypeScript:

const key = 'BTC-USD';
  const [fcdPda, seed] = await derivePDAFromFCDKey(key, new PublicKey(chainProgramId));

  let tx = await callerProgram.methods
    .readFcd()
    .accounts({
      fcd: fcdPda,
    })
    .rpc({commitment: 'confirmed'});

  const solanaProvider = new Connection(process.env.ANCHOR_PROVIDER_URL);
  console.log(await getReturnLog(solanaProvider, tx))

Verifying Layer-2 Data

In order to verify the L2D, we will need first to retrieve the PDA for the account storing the information of the specific block the L2D belongs to. This is done by using the block ID as seed for the `FindProgramAddress' function.

Follows an example for retrieving account for block id: 505332

const programId = "4SPgs3L7Ey9VyRuZwx4X3y86LSAZXP2Hhpz9Sps4v3iT";
let block_id = 505332;
const seed = LeafValueCoder.encode(block_id, '');
const [blockPda, bump] = await PublicKey.findProgramAddress(
    [seed], program.programId
);
let block = await program.account.authority.fetch(blockPda);

For actually verifiying the proofs for a L2D we need to invoke the verify_proof_for_block function in the Chain program. This function returns TRUE if the proofs were verified (meaning the value for the indicated key is teh one validated by the Oracle for provided block id) or FALSE if not,

In code, this can be performed as:

await program.methods
  .verifyProofForBlock(seed, proofs, key, value)
  .accounts({
    verifyResult: verifyResultAccount.publicKey,
    block: blockPda,
  })
  .rpc({ commitment: 'confirmed' });

// we check the result
const account = await program.account.verifyResult.fetch(verifyResultAccount.publicKey);
console.log('Verify result =', account.result);

where:

  • seed: is the block id encoded as bytes
  • key: is the key of the L2D to verify (e.g. "BTC-USD") encoded as bytes
  • value: is the value (associated to the key) encoded as bytes
  • proofs: is an array of the proofs for that particular key-value pair

The only way Solana programs can return on-chain information to the real world is also through the use of accounts, which means that the result of the verify_proof_for_block should be written on an account. For this purpose, the VerifyResult account must be created.

The following code generates a VerifyAccount account which will be use to store the result of the L2D verification:

const verifyResultAccount = anchor.web3.Keypair.generate();

await program.methods
  .initializeVerifyResult()
  .accounts({
    verifyResult: verifyResultAccount.publicKey,
    user: provider.wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .signers([verifyResultAccount])
  .rpc({ commitment: 'confirmed' });

where:

  • verifyResult: is the address (public key) that the account will have (in this case it is a random generated address)
  • user: is the address (public key) of the account that will provide the funds for creating this account
  • systemProgram: is the PDA of a Solana program used to initialize accounts

The next figure illustrates the process described above:

14301430

Block validation through cross-program invocation (CPI)

In a similar way, it is also possible to verify data through another program on Solana. This second program could be for example a DEX or any other dApp that wants to confirm a price retrieved for a L2D is correct. For this purpose, the caller program will perform a cross-program invocation to our chain program. In turn, the latter will write the result also on the VerifyResult account or return it directly to the caller program for internal use.

14301430

Did this page help you?