Switchboard

Switchboardは、開発者が価格フィード、NFT 最低価格、スポーツ統計、さらには検証可能なランダム性など、さまざまなユースケースのためにチェーン上でデータを調達できるようにする Oracle プロトコルです。 一般的な意味で、Switchboardはオフチェーンのリソースであり、開発者はチェーン上で整合性の高いデータを橋渡しし、次世代の web3とDeFiを強化するために呼び出すことができます。

Data Feeds

Switchboardは @switchboard-xyz/switchboard-v2 として呼び出されるJavaScript/TypeScriptライブラリです。このライブラリを使用して、既存のデータフィードからオンチェーン データにアクセスしたり、独自のカスタムフィードを公開したりできます。詳細はこちらopen in new window

アグリゲーターフィードからデータを読み取る

Press </> button to view full source
import {
  clusterApiUrl,
  Connection,
  Keypair,
  PublicKey,
  LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import * as switchboard from "@switchboard-xyz/switchboard-v2";
async () => {
  const payer = Keypair.generate();
  const connection = new Connection(clusterApiUrl("devnet"));
  const program = await switchboard.loadSwitchboardProgram(
    "devnet",
    connection,
    payer
  );
  const airdropSignature = await connection.requestAirdrop(
    payer.publicKey,
    LAMPORTS_PER_SOL
  );

  await connection.confirmTransaction(airdropSignature);
  console.log("here");
  const aggregatorAccount = new switchboard.AggregatorAccount({
    program: program,
    publicKey: aggregatorKey,
  });
  const result: any = await aggregatorAccount.getLatestValue();

  console.log(result.toNumber());
};

アグリゲーターフィードを新規作成する

Press </> button to view full source
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
  AggregatorAccount,
  loadSwitchboardProgram,
  LeaseAccount,
  OracleQueueAccount,
  SwitchboardPermission,
} from "@switchboard-xyz/switchboard-v2";

let payer: Keypair;
let authority: Keypair; // queue authority
const program = await loadSwitchboardProgram("devnet", undefined, payer);
const queueAccount = new OracleQueueAccount({
  program,
  publicKey: queuePubkey,
});

// aggregator
const aggregatorAccount = await AggregatorAccount.create(program, {
  name: Buffer.from("MY SOL/USD Feed"),
  batchSize: 1,
  minRequiredOracleResults: 1,
  minRequiredJobResults: 1,
  minUpdateDelaySeconds: 10,
  queueAccount,
  authority: authority.publicKey,
});

// permission
const permissionAccount = await PermissionAccount.create(program, {
  authority: authority.publicKey,
  granter: queueAccount.publicKey,
  grantee: aggregatorAccount.publicKey,
});
await aggregatorPermission.set({
  authority,
  permission: SwitchboardPermission.PERMIT_ORACLE_QUEUE_USAGE,
  enable: true,
});

// lease
const leaseContract = await LeaseAccount.create(program, {
  loadAmount: new anchor.BN(0),
  funder: tokenAccount,
  funderAuthority: authority,
  oracleQueueAccount: queueAccount,
  aggregatorAccount,
});

// job
const tasks: OracleJob.Task[] = [
  OracleJob.Task.create({
    httpTask: OracleJob.HttpTask.create({
      url: `https://ftx.us/api/markets/SOL_USD`,
    }),
  }),
  OracleJob.Task.create({
    jsonParseTask: OracleJob.JsonParseTask.create({ path: "$.result.price" }),
  }),
];
const jobData = Buffer.from(
  OracleJob.encodeDelimited(
    OracleJob.create({
      tasks,
    })
  ).finish()
);
const jobKeypair = anchor.web3.Keypair.generate();
const jobAccount = await JobAccount.create(program, {
  data: jobData,
  keypair: jobKeypair,
  authority: authority.publicKey,
});

// add job to aggregator
await aggregatorAccount.addJob(jobAccount, authority);

プログラムでアグリゲーターフィードからデータを読み取る

Switchboardはswitchboard_v2というクレートを提供します。詳細については、こちらopen in new windowをご覧ください。

Press </> button to view full source
use anchor_lang::prelude::*;

use switchboard_v2::AggregatorAccountData;

declare_id!("HDa6A4wLjEymb8Cv3UGeGBnFUNCUEMpoiVBbkTKAkfrt");

#[program]
pub mod get_result {
    use super::*;
   
    pub fn get_result(ctx: Context<GetResult>) -> Result<()> {
        let aggregator = &ctx.accounts.aggregator_feed.load()?;
        let val:f64 = aggregator
            .get_result()?
            .try_into()?;

        msg!("Current feed result is {}!", val);
        Ok(())
    }
   
}

#[derive(Accounts)]
pub struct GetResult<'info> {
    pub authority: Signer<'info>,
    /// CHECK: field is unsafe
    pub aggregator_feed: AccountLoader<'info, AggregatorAccountData>, // pass aggregator key
}

パブリッシャーからフィードを作成する方法

公式の Switchboard ドキュメントには、パブリッシャーからフィードを作成する方法の詳細なウォークスルーがあります。こちらopen in new windowをチェックしてください。

Oracle

Switchboardのユニークな機能は、独自のOracleを作成してローカルで実行できることです。

Oracleを作成

Press </> button to view full source
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
  loadSwitchboardProgram,
  OracleAccount,
  OracleQueueAccount,
} from "@switchboard-xyz/switchboard-v2";

let payer: Keypair;
const program = await loadSwitchboardProgram("devnet", undefined, payer);

const queueAccount = new OracleQueueAccount({
  program,
  publicKey: queuePubkey,
});

// Create oracle
const oracleAccount = await OracleAccount.create(program, {
  name: Buffer.from("My Oracle"),
  queueAccount,
});

Oracleをローカルで実行

Oracleをローカルで実行し、独自のOracleキューに割り当てて、プログラムが本番環境でどのように動作するかをテストできます。メインネットのOracleは、監視機能のセットを備えた高可用性環境で常に実行する必要があります。

要件

  • Docker-compose

Oracle Configで環境変数を使用してdocker-compose.ymlを作成します。

Press </> button to view full source
version: "3.3"
services:
  switchboard:
    image: "switchboardlabs/node:dev-v2-5-28-22a"
    network_mode: host
    restart: always
    environment:
      - LIVE=1
      - CLUSTER=devnet
      - RPC_URL=${RPC_URL}
      - ORACLE_KEY=${ORACLE_KEY}
      - HEARTBEAT_INTERVAL=15
    volumes:
      - ./configs.json:/configs.json
secrets:
  PAYER_SECRETS:
    file: /filesystem/path/to/keypair.json

docker-compose upを使用してコンテナを実行します。

Oracle Config

Env VariableDefinition
ORACLE_KEYRequired
Type - Public Key
Description - Oracleキューを使用する権限を付与されたOracleアカウントの公開鍵
HEARTBEAT_INTERVALOptional
Type - Number (seconds)
Default - 30
Description - Oracle ハートビート間の秒数。キューには、異なるOracleハートビート要件があります。推奨値は15です
GCP_CONFIG_BUCKETOptional
Type - GCP Resource Path
Default - 現在の作業ディレクトリで configs.json を探します。見つからない場合、構成はロードされません。
Description - プライベートAPIエンドポイントのAPIキーを含む
UNWRAP_STAKE_THRESHOLDOptional
Type - Number (SOL amount, Ex. 1.55)
Default - 0, disabled.
Description - unwrap stake actionをトリガーする Solana の残高。OracleのSolana 残高が設定されたしきい値を下回ると、ノードは自動的にオラクルのステーキングウォレットから資金をアンラップし、少なくとも0.1 wSOL またはキューの最小ステーク要件より10%多い金額を残します。

検証可能な確率関数(VRF)

検証可能な確率関数(Verifiable Random Function/VRF)出力が正しく計算されたことを証明する公開鍵疑似乱数関数です。

VRFアカウントの読み取り

Press </> button to view full source
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
  loadSwitchboardProgram,
  VrfAccount,
} from "@switchboard-xyz/switchboard-v2";

let payer: Keypair;
const program = await loadSwitchboardProgram("devnet", undefined, payer);

const vrfAccount = new VrfAccount({
  program,
  publicKey: vrfKey,
});
const vrf = await vrfAccount.loadData();
console.log(vrf.currentRound.result);
use switchboard_v2::VrfAccountData;

let vrf = VrfAccountData::new(vrf_account_info)?;
let result_buffer = vrf.get_result()?;
if result_buffer == [0u8; 32] {
    msg!("vrf buffer empty");
    return Ok(());
}

let value: &[u128] = bytemuck::cast_slice(&result_buffer[..]);
let result = value[0] % 256000 as u128;

VRFアカウントの作成

Press </> button to view full source
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
  loadSwitchboardProgram,
  OracleQueueAccount,
  PermissionAccount,
  SwitchboardPermission,
  VrfAccount,
} from "@switchboard-xyz/switchboard-v2";

let payer: Keypair;
const program = await loadSwitchboardProgram("devnet", undefined, payer);
const queueAccount = new queueAccount({ program, publicKey: queueKey });
const queue = await queueAccount.loadData();

// load client program used for callback
const vrfClientProgram = anchor.workspace
  .AnchorVrfParser as anchor.Program<AnchorVrfParser>;
const vrfSecret = anchor.web3.Keypair.generate();

const vrfIxCoder = new anchor.BorshInstructionCoder(vrfClientProgram.idl);
const vrfClientCallback: Callback = {
  programId: vrfClientProgram.programId,
  accounts: [
    // ensure all accounts in updateResult are populated
    { pubkey: vrfClientKey, isSigner: false, isWritable: true },
    { pubkey: vrfSecret.publicKey, isSigner: false, isWritable: false },
  ],
  ixData: vrfIxCoder.encode("updateResult", ""), // pass any params for instruction here
};

// create VRF
const vrfAccount = await VrfAccount.create(program, {
  queue: queueAccount,
  callback: vrfClientCallback,
  authority: vrfClientKey, // vrf authority
  keypair: vrfSecret,
});

// create permission
const permissionAccount = await PermissionAccount.create(program, {
  authority: queue.authority,
  granter: queue.publicKey,
  grantee: vrfAccount.publicKey,
});

// if queue has not enabled unpermissionedVrfEnabled, queue will need to grant permission
let queueAuthority: Keypair;
await permissionAccount.set({
  authority: queueAuthority,
  permission: SwitchboardPermission.PERMIT_VRF_REQUESTS,
  enable: true,
});

VRFアカウントからRandomnessを要求する

Press </> button to view full source
import * as anchor from "@project-OpenBook/anchor";
import { Keypair } from "@solana/web3.js";
import {
  loadSwitchboardProgram,
  VrfAccount,
} from "@switchboard-xyz/switchboard-v2";

let payer: Keypair;
let authority: Keypair;
const program = await loadSwitchboardProgram("devnet", undefined, payer);

const vrfAccount = new VrfAccount({
  program,
  publicKey: vrfKey,
});
const vrf = await vrfAccount.loadData();

const queueAccount = new OracleQueueAccount({
  program,
  publicKey: vrf.queuePubkey,
});
const queue = await queueAccount.loadData();
const mint = await queueAccount.loadMint();

const payerTokenWallet = (
  await mint.getOrCreateAssociatedAccountInfo(payer.publicKey)
).address;

const signature = await vrfAccount.requestRandomness({
  authority,
  payer: payerTokenWallet,
  payerAuthority: payer,
});
use crate::*;
use anchor_lang::prelude::*;
pub use switchboard_v2::{VrfAccountData, VrfRequestRandomness};
use anchor_spl::token::Token;
use anchor_lang::solana_program::clock;

#[derive(Accounts)]
#[instruction(params: RequestResultParams)] // rpc parameters hint
pub struct RequestResult<'info> {
    #[account(
        mut,
        seeds = [
            STATE_SEED,
            vrf.key().as_ref(),
            authority.key().as_ref(),
        ],
        bump = state.load()?.bump,
        has_one = vrf,
        has_one = authority
    )]
    pub state: AccountLoader<'info, VrfClient>,
    #[account(signer)]
    pub authority: AccountInfo<'info>,
    #[account(constraint = switchboard_program.executable == true)]
    pub switchboard_program: AccountInfo<'info>,
    #[account(mut, constraint = vrf.owner.as_ref() == switchboard_program.key().as_ref())]
    pub vrf: AccountInfo<'info>,
    #[account(mut, constraint = oracle_queue.owner.as_ref() == switchboard_program.key().as_ref())]
    pub oracle_queue: AccountInfo<'info>,
    pub queue_authority: UncheckedAccount<'info>,
    #[account(constraint = data_buffer.owner.as_ref() == switchboard_program.key().as_ref())]
    pub data_buffer: AccountInfo<'info>,
    #[account(mut, constraint = permission.owner.as_ref() == switchboard_program.key().as_ref())]
    pub permission: AccountInfo<'info>,
    #[account(mut, constraint = escrow.owner == program_state.key())]
    pub escrow: Account<'info, TokenAccount>,
    #[account(mut, constraint = payer_wallet.owner == payer_authority.key())]
    pub payer_wallet: Account<'info, TokenAccount>,
    #[account(signer)]
    pub payer_authority: AccountInfo<'info>,
    #[account(address = solana_program::sysvar::recent_blockhashes::ID)]
    pub recent_blockhashes: AccountInfo<'info>,
    #[account(constraint = program_state.owner.as_ref() == switchboard_program.key().as_ref())]
    pub program_state: AccountInfo<'info>,
    #[account(address = anchor_spl::token::ID)]
    pub token_program: Program<'info, Token>,
}

#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct RequestResultParams {
    pub permission_bump: u8,
    pub switchboard_state_bump: u8,
}

impl RequestResult<'_> {
    pub fn validate(&self, _ctx: &Context<Self>, _params: &RequestResultParams) -> Result<()> {
        Ok(())
    }

    pub fn actuate(ctx: &Context<Self>, params: &RequestResultParams) -> Result<()> {
        let client_state = ctx.accounts.state.load()?;
        let bump = client_state.bump.clone();
        let max_result = client_state.max_result.clone();
        drop(client_state);

        let switchboard_program = ctx.accounts.switchboard_program.to_account_info();

        let vrf_request_randomness = VrfRequestRandomness {
            authority: ctx.accounts.state.to_account_info(),
            vrf: ctx.accounts.vrf.to_account_info(),
            oracle_queue: ctx.accounts.oracle_queue.to_account_info(),
            queue_authority: ctx.accounts.queue_authority.to_account_info(),
            data_buffer: ctx.accounts.data_buffer.to_account_info(),
            permission: ctx.accounts.permission.to_account_info(),
            escrow: ctx.accounts.escrow.clone(),
            payer_wallet: ctx.accounts.payer_wallet.clone(),
            payer_authority: ctx.accounts.payer_authority.to_account_info(),
            recent_blockhashes: ctx.accounts.recent_blockhashes.to_account_info(),
            program_state: ctx.accounts.program_state.to_account_info(),
            token_program: ctx.accounts.token_program.to_account_info(),
        };

        let vrf_key = ctx.accounts.vrf.key.clone();
        let authority_key = ctx.accounts.authority.key.clone();

        msg!("bump: {}", bump);
        msg!("authority: {}", authority_key);
        msg!("vrf: {}", vrf_key);

        let state_seeds: &[&[&[u8]]] = &[&[
            &STATE_SEED,
            vrf_key.as_ref(),
            authority_key.as_ref(),
            &[bump],
        ]];
        msg!("requesting randomness");
        vrf_request_randomness.invoke_signed(
            switchboard_program,
            params.switchboard_state_bump,
            params.permission_bump,
            state_seeds,
        )?;

        emit!(RequestingRandomness{
            vrf_client: ctx.accounts.state.key(),
            max_result: max_result,
            timestamp: clock::Clock::get().unwrap().unix_timestamp
        });

        msg!("randomness requested successfully");
        Ok(())
    }
}

参考文献

APIs and Libraries

Examples

More Information

Last Updated:
Contributors: PokoPoko2ry