prelim implementation of oracle submit

This commit is contained in:
De Facto 2021-02-03 19:33:45 +08:00
parent c3c5028ff1
commit ebdeff24e6
5 changed files with 209 additions and 130 deletions

View File

@ -17,7 +17,8 @@ fn process_instruction<'a>(
) -> ProgramResult {
if let Err(error) = Processor::process(program_id, accounts, instruction_data) {
// catch the error so we can print it
error.print::<Error>();
// error.print::<Error>();
// msg!("{:?}", error);
return Err(error);
}
Ok(())

View File

@ -45,41 +45,51 @@ pub enum Error {
InsufficientWithdrawable,
/// Aggregator key not match
#[error("Aggregator key not match")]
AggregatorKeyNotMatch,
/// Max oralces reached
#[error("Max oracles reached")]
MaxOralcesReached,
AggregatorMismatch,
#[error("Invalid round id")]
InvalidRoundID,
#[error("Cannot start new round until cooldown")]
OracleNewRoundCooldown,
#[error("Max number of submissions reached for this round")]
MaxSubmissionsReached,
#[error("Each oracle may only submit once per round")]
OracleAlreadySubmitted,
}
impl PrintProgramError for Error {
fn print<E>(&self)
where
E: 'static + std::error::Error + DecodeError<E> + PrintProgramError + FromPrimitive,
{
match self {
Error::InvalidInstruction => msg!("Error: Invalid instruction"),
Error::AlreadyInUse => msg!("Error: Already in use"),
Error::NotRentExempt => msg!("Error: No rent exempt"),
Error::NotFoundAggregator => msg!("Error: no found aggregator"),
Error::OracleExist => msg!("Error: Oracle exist"),
Error::OwnerMismatch => msg!("Error: Owner mismatch"),
Error::NotFoundOracle => msg!("Error: Not found oracle"),
Error::SubmissonValueOutOfRange => msg!("Error: Submisson value out of range"),
Error::SubmissonCooling => msg!("Submission cooling"),
Error::InsufficientWithdrawable => msg!("Insufficient withdrawable"),
Error::AggregatorKeyNotMatch => msg!("Aggregator key not match"),
Error::MaxOralcesReached => msg!("Max oracles reached"),
}
}
}
// impl PrintProgramError for Error {
// fn print<E>(&self)
// where
// E: 'static + std::error::Error + DecodeError<E> + PrintProgramError + FromPrimitive,
// {
// match self {
// Error::InvalidInstruction => msg!("Error: Invalid instruction"),
// Error::AlreadyInUse => msg!("Error: Already in use"),
// Error::NotRentExempt => msg!("Error: No rent exempt"),
// Error::NotFoundAggregator => msg!("Error: no found aggregator"),
// Error::OracleExist => msg!("Error: Oracle exist"),
// Error::OwnerMismatch => msg!("Error: Owner mismatch"),
// Error::NotFoundOracle => msg!("Error: Not found oracle"),
// Error::SubmissonValueOutOfRange => msg!("Error: Submisson value out of range"),
// Error::SubmissonCooling => msg!("Submission cooling"),
// Error::InsufficientWithdrawable => msg!("Insufficient withdrawable"),
// Error::AggregatorMismatch => msg!("Aggregator key not match"),
// Error::MaxOralcesReached => msg!("Max oracles reached"),
// }
// }
// }
impl From<Error> for ProgramError {
fn from(e: Error) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for Error {
fn type_of() -> &'static str {
"Error"
}
}
// impl<T> DecodeError<T> for Error {
// fn type_of() -> &'static str {
// "Error"
// }
// }

View File

@ -27,16 +27,9 @@ pub enum Instruction {
/// Remove an oracle
RemoveOracle,
/// Called by oracles when they have witnessed a need to update
///
/// Accounts expected by this instruction:
/// 0. `[writable]` The aggregator(key).
/// 1. `[]` Clock sysvar
/// 2. `[writable]` The oracle key.
/// 3. `[signer]` The oracle owner.
Submit {
/// the updated data that the oracle is submitting
submission: u64,
round_id: u64,
value: u64,
},
/// Oracle withdraw token

View File

@ -1,6 +1,8 @@
//! Program state processor
use crate::{error::Error, instruction::{Instruction, PAYMENT_AMOUNT}, state::{Aggregator, AggregatorConfig, Oracle}};
use std::default;
use crate::{error::Error, instruction::{Instruction, PAYMENT_AMOUNT}, state::{Aggregator, AggregatorConfig, Oracle, Round}};
use solana_program::{
account_info::{next_account_info, AccountInfo},
@ -9,7 +11,7 @@ use solana_program::{
msg,
program::invoke_signed,
program_error::ProgramError,
program_pack::Pack,
program_pack::{IsInitialized},
pubkey::Pubkey,
sysvar::{rent::Rent, Sysvar},
};
@ -30,6 +32,10 @@ impl <'a, 'b>Accounts<'a, 'b> {
fn get_rent(&self, i: usize) -> Result<Rent, ProgramError> {
Rent::from_account_info(self.get(i)?)
}
fn get_clock(&self, i: usize) -> Result<Clock, ProgramError> {
Clock::from_account_info(self.get(i)?)
}
}
struct InitializeContext<'a> {
@ -108,7 +114,7 @@ impl <'a>RemoveOracleContext<'a> {
let oracle = Oracle::load_initialized(self.oracle)?;
if oracle.aggregator != self.aggregator.key.to_bytes() {
return Err(Error::OwnerMismatch)?;
return Err(Error::AggregatorMismatch)?;
}
// Zero out the oracle account memory. This allows reuse or reclaim.
@ -119,6 +125,122 @@ impl <'a>RemoveOracleContext<'a> {
}
}
struct SubmitContext<'a> {
clock: Clock,
aggregator: &'a AccountInfo<'a>,
oracle: &'a AccountInfo<'a>,
oracle_owner: &'a AccountInfo<'a>, // signed
// NOTE: 5.84942*10^11 years even if 1 sec per round. don't bother with handling wrapparound.
round_id: u64,
value: u64,
}
impl <'a>SubmitContext<'a> {
fn process(&self) -> ProgramResult {
let mut aggregator = Aggregator::load_initialized(self.aggregator)?;
let mut oracle = Oracle::load_initialized(self.oracle)?;
if !self.oracle_owner.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
if oracle.aggregator != self.aggregator.key.to_bytes() {
return Err(Error::AggregatorMismatch)?;
}
// oracle starts a new round
if self.round_id == aggregator.current_round.id + 1 {
self.start_new_round(&mut aggregator, &mut oracle)?;
}
// only allowed to submit in the current round (or a new round that just
// got started)
if self.round_id != aggregator.current_round.id {
return Err(Error::InvalidRoundID)?;
}
self.submit(&mut aggregator, &oracle)?;
// credit oracle for submission
oracle.withdrawable = aggregator.config.reward_amount;
aggregator.save(self.aggregator)?;
oracle.save(self.oracle)?;
Ok(())
}
/// push oracle answer to the current round. update answer if min submissions
/// had been satisfied.
fn submit(&self, aggregator: &mut Aggregator, oracle: &Oracle) -> ProgramResult {
let now = self.clock.unix_timestamp as u64;
let (i, submission) = aggregator.current_round.submissions.iter_mut().enumerate().find(|(i, s)| {
// either finds a new spot to put the submission, or find a spot
// that the oracle previously submitted to.
return !s.is_initialized() || s.oracle == oracle.owner;
}).ok_or(Error::MaxSubmissionsReached)?;
let count = i + 1;
if count > aggregator.config.max_submissions as usize {
return Err(Error::MaxSubmissionsReached)?;
}
if submission.is_initialized() {
return Err(Error::OracleAlreadySubmitted)?;
}
submission.updated_at = now;
submission.value = self.value;
submission.oracle = self.oracle.key.to_bytes();
if count < aggregator.config.min_submissions as usize {
// not enough submissions to update answer. return now.
return Ok(());
}
let new_submission = *submission;
// update answer if the new round reached min_submissions
let round = &aggregator.current_round;
let answer = &mut aggregator.answer;
if round.id != answer.round_id {
// a new round had just been resolved. copy the current round's submissions over
answer.round_id = round.id;
answer.created_at = now;
answer.updated_at = now;
answer.submissions = round.submissions;
} else {
answer.updated_at = now;
answer.submissions[i] = new_submission;
}
Ok(())
}
fn start_new_round(&self, aggregator: &mut Aggregator, oracle: &mut Oracle) -> ProgramResult {
let now = self.clock.unix_timestamp as u64;
if oracle.allow_start_round <= aggregator.current_round.id {
return Err(Error::OracleNewRoundCooldown)?;
}
// zero the submissions of the current round
aggregator.current_round = Round {
id: self.round_id,
started_at: now,
..Round::default()
};
// oracle can start new round after `1 + restart_delay` rounds
oracle.allow_start_round = self.round_id + aggregator.config.restart_delay + 1;
Ok(())
}
}
/// Program state handler.
pub struct Processor {}
@ -156,8 +278,19 @@ impl Processor {
oracle: accounts.get(2)?,
}.process()
},
Instruction::Submit { round_id, value } => {
SubmitContext {
clock: accounts.get_clock(0)?,
aggregator: accounts.get(1)?,
oracle: accounts.get(2)?,
oracle_owner: accounts.get(3)?,
round_id,
value
}.process()
},
_ => {
Ok(())
Err(ProgramError::InvalidInstructionData)
}
// Instruction::Submit { submission } => {
// msg!("Instruction: Submit");
@ -170,75 +303,6 @@ impl Processor {
}
}
// /// Processes an [Submit](enum.Instruction.html) instruction.
// pub fn process_submit(accounts: &[AccountInfo], submission: u64) -> ProgramResult {
// let account_info_iter = &mut accounts.iter();
// let aggregator_info = next_account_info(account_info_iter)?;
// let clock_sysvar_info = next_account_info(account_info_iter)?;
// let oracle_info = next_account_info(account_info_iter)?;
// let oracle_owner_info = next_account_info(account_info_iter)?;
// let mut aggregator = Aggregator::unpack_unchecked(&aggregator_info.data.borrow())?;
// if !aggregator.is_initialized {
// return Err(Error::NotFoundAggregator.into());
// }
// if submission < aggregator.min_submission_value
// || submission > aggregator.max_submission_value
// {
// return Err(Error::SubmissonValueOutOfRange.into());
// }
// if !oracle_owner_info.is_signer {
// return Err(ProgramError::MissingRequiredSignature);
// }
// let mut oracle = Oracle::unpack_unchecked(&oracle_info.data.borrow())?;
// if !oracle.is_initialized {
// return Err(Error::NotFoundOracle.into());
// }
// if &Pubkey::new_from_array(oracle.owner) != oracle_owner_info.key {
// return Err(Error::OwnerMismatch.into());
// }
// if &Pubkey::new_from_array(oracle.aggregator) != aggregator_info.key {
// return Err(Error::AggregatorKeyNotMatch.into());
// }
// let clock = &Clock::from_account_info(clock_sysvar_info)?;
// // check whether the aggregator owned this oracle
// let mut found = false;
// for s in aggregator.submissions.iter_mut() {
// if &Pubkey::new_from_array(s.oracle) == oracle_info.key {
// s.value = submission;
// s.time = clock.unix_timestamp;
// found = true;
// break;
// }
// }
// if !found {
// return Err(Error::NotFoundOracle.into());
// }
// if oracle.next_submit_time > clock.unix_timestamp {
// return Err(Error::SubmissonCooling.into());
// }
// oracle.withdrawable += PAYMENT_AMOUNT;
// oracle.next_submit_time = clock.unix_timestamp + aggregator.submit_interval as i64;
// // update aggregator
// Aggregator::pack(aggregator, &mut aggregator_info.data.borrow_mut())?;
// // update oracle
// Oracle::pack(oracle, &mut oracle_info.data.borrow_mut())?;
// Ok(())
// }
// /// Processes an [Withdraw](enum.Instruction.html) instruction
// pub fn process_withdraw(
// accounts: &[AccountInfo],
@ -407,11 +471,9 @@ mod tests {
&program_id,
instruction::Instruction::Initialize {
config: AggregatorConfig{
submit_interval: 10,
min_submission_value: 0,
max_submission_value: 100,
submission_decimals: 8,
decimals: 8,
description: [0u8; 32],
..AggregatorConfig::default()
}
},
vec![

View File

@ -4,23 +4,27 @@ use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use crate::instruction::MAX_ORACLES;
use crate::borsh_state::{InitBorshState, BorshState};
use solana_program::{
clock::UnixTimestamp,
program_pack::IsInitialized,
};
use solana_program::{program_pack::IsInitialized};
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
pub struct AggregatorConfig {
/// The interval(seconds) of an oracle's each submission
pub submit_interval: u32,
/// min submission value
pub min_submission_value: u64,
/// max submission value
pub max_submission_value: u64,
/// submission decimals
pub submission_decimals: u8,
/// decimals for this feed
pub decimals: u8,
/// description
pub description: [u8; 32],
/// oracle cannot start a new round until after `restart_relay` rounds
pub restart_delay: u64,
/// amount of tokens oracles are reward per submission
pub reward_amount: u64,
/// max number of submissions in a round
pub max_submissions: u8,
/// min number of submissions in a round to resolve an answer
pub min_submissions: u8,
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
@ -65,13 +69,18 @@ impl InitBorshState for Aggregator {}
#[derive(Clone, Copy, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
pub struct Submission {
/// submit time
pub time: UnixTimestamp,
pub updated_at: u64,
/// value
pub value: u64,
/// oracle
pub oracle: [u8; 32],
}
impl IsInitialized for Submission {
fn is_initialized(&self) -> bool {
self.updated_at > 0
}
}
/// Oracle data.
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
@ -82,6 +91,10 @@ pub struct Oracle {
pub is_initialized: bool,
/// withdrawable
pub withdrawable: u64,
/// oracle cannot start a new round until after `restart_relay` rounds
pub allow_start_round: u64,
/// aggregator
pub aggregator: [u8; 32],
/// owner