move submissions out of Aggregator struct

This commit is contained in:
De Facto 2021-02-05 16:33:48 +08:00
parent 8b95bfa4f7
commit 01a910e7aa
3 changed files with 231 additions and 92 deletions

View File

@ -25,17 +25,21 @@ pub struct ResolvedMedian {
pub created_at: u64,
}
/// Read resolved median value from the aggregator
pub fn read_median(aggregator_info: &AccountInfo) -> Result<ResolvedMedian, ProgramError> {
/// Read resolved median value from the aggregator answer submissions
pub fn read_median(
aggregator_info: &AccountInfo,
answer_submissions_info: &AccountInfo,
) -> Result<ResolvedMedian, ProgramError> {
let aggregator = Aggregator::load_initialized(&aggregator_info)?;
if !aggregator.answer.is_initialized() {
return Err(Error::NoResolvedAnswer)?;
}
let mut values: Vec<_> = aggregator
.answer
.submissions
let submissions = aggregator.answer_submissions(answer_submissions_info)?;
let mut values: Vec<_> = submissions
.data
.iter()
.filter(|s| s.is_initialized())
.map(|s| s.value)

View File

@ -3,7 +3,7 @@
use crate::{
error::Error,
instruction::Instruction,
state::{Aggregator, AggregatorConfig, Authority, Oracle, Round},
state::{Aggregator, AggregatorConfig, Authority, Oracle, Round, Submissions},
};
use solana_program::{
@ -43,6 +43,8 @@ struct InitializeContext<'a> {
rent: Rent,
aggregator: &'a AccountInfo<'a>,
aggregator_owner: &'a AccountInfo<'a>,
round_submissions: &'a AccountInfo<'a>, // belongs_to: aggregator
answer_submissions: &'a AccountInfo<'a>, // belongs_to: aggregator
config: AggregatorConfig,
}
@ -53,14 +55,29 @@ impl<'a> InitializeContext<'a> {
return Err(ProgramError::MissingRequiredSignature);
}
self.init_submissions(self.round_submissions)?;
self.init_submissions(self.answer_submissions)?;
let mut aggregator = Aggregator::init_uninitialized(self.aggregator)?;
aggregator.is_initialized = true;
aggregator.config = self.config.clone();
aggregator.owner = self.aggregator_owner.key.to_bytes();
aggregator.owner = self.aggregator_owner.into();
// aggregator.round_submissions = PublicKey(self.round_submissions.key.to_bytes());
aggregator.round_submissions = self.round_submissions.into();
aggregator.answer_submissions = self.answer_submissions.into();
aggregator.save_exempt(self.aggregator, &self.rent)?;
Ok(())
}
fn init_submissions(&self, account: &AccountInfo) -> ProgramResult {
let mut submissions = Submissions::init_uninitialized(account)?;
submissions.is_initialized = true;
submissions.save_exempt(account, &self.rent)?;
Ok(())
}
}
struct ConfigureContext<'a> {
@ -100,8 +117,8 @@ impl<'a> AddOracleContext<'a> {
let mut oracle = Oracle::init_uninitialized(self.oracle)?;
oracle.is_initialized = true;
oracle.description = self.description;
oracle.owner = self.oracle_owner.key.to_bytes();
oracle.aggregator = self.aggregator.key.to_bytes();
oracle.owner = self.oracle_owner.into();
oracle.aggregator = self.aggregator.into();
oracle.save_exempt(self.oracle, &self.rent)?;
Ok(())
@ -120,7 +137,7 @@ impl<'a> RemoveOracleContext<'a> {
aggregator.authorize(self.aggregator_owner)?;
let oracle = Oracle::load_initialized(self.oracle)?;
if oracle.aggregator != self.aggregator.key.to_bytes() {
if oracle.aggregator.0 != self.aggregator.key.to_bytes() {
return Err(Error::AggregatorMismatch)?;
}
@ -135,6 +152,8 @@ impl<'a> RemoveOracleContext<'a> {
struct SubmitContext<'a> {
clock: Clock,
aggregator: &'a AccountInfo<'a>,
round_submissions: &'a AccountInfo<'a>,
answer_submissions: &'a AccountInfo<'a>,
oracle: &'a AccountInfo<'a>,
oracle_owner: &'a AccountInfo<'a>, // signed
@ -149,18 +168,18 @@ impl<'a> SubmitContext<'a> {
let mut oracle = Oracle::load_initialized(self.oracle)?;
oracle.authorize(self.oracle_owner)?;
if oracle.aggregator != self.aggregator.key.to_bytes() {
if oracle.aggregator.0 != self.aggregator.key.to_bytes() {
return Err(Error::AggregatorMismatch)?;
}
// oracle starts a new round
if self.round_id == aggregator.current_round.id + 1 {
if self.round_id == aggregator.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 {
if self.round_id != aggregator.round.id {
return Err(Error::InvalidRoundID)?;
}
@ -183,9 +202,10 @@ impl<'a> SubmitContext<'a> {
fn submit(&self, aggregator: &mut Aggregator) -> ProgramResult {
let now = self.clock.unix_timestamp as u64;
let (i, submission) = aggregator
.current_round
.submissions
let mut round_submissions = aggregator.round_submissions(self.round_submissions)?;
let (i, submission) = round_submissions
.data
.iter_mut()
.enumerate()
.find(|(_i, s)| {
@ -205,23 +225,28 @@ impl<'a> SubmitContext<'a> {
return Err(Error::OracleAlreadySubmitted)?;
}
if aggregator.current_round.created_at == 0 {
aggregator.current_round.created_at = now;
if aggregator.round.created_at == 0 {
aggregator.round.created_at = now;
}
aggregator.current_round.updated_at = now;
aggregator.round.updated_at = now;
submission.updated_at = now;
submission.value = self.value;
submission.oracle = self.oracle.key.to_bytes();
// this line is for later, but put here to deal with borrow check...
let new_submission = *submission;
round_submissions.save(self.round_submissions)?;
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 mut answer_submissions = aggregator.answer_submissions(self.answer_submissions)?;
let round = &aggregator.round;
let answer = &mut aggregator.answer;
if !answer.is_initialized() || round.id > answer.round_id {
@ -229,29 +254,37 @@ impl<'a> SubmitContext<'a> {
answer.round_id = round.id;
answer.created_at = now;
answer.updated_at = now;
answer.submissions = round.submissions;
answer_submissions.data = round_submissions.data;
} else {
answer.updated_at = now;
answer.submissions[i] = new_submission;
answer_submissions.data[i] = new_submission;
}
answer_submissions.save(self.answer_submissions)?;
Ok(())
}
fn start_new_round(&self, aggregator: &mut Aggregator, oracle: &mut Oracle) -> ProgramResult {
let now = self.clock.unix_timestamp as u64;
if aggregator.current_round.id < oracle.allow_start_round {
if aggregator.round.id < oracle.allow_start_round {
return Err(Error::OracleNewRoundCooldown)?;
}
// zero the submissions of the current round
aggregator.current_round = Round {
aggregator.round = Round {
id: self.round_id,
created_at: now,
..Round::default()
updated_at: 0,
};
// zero the submissions of the current round
let submissions = Submissions {
is_initialized: true,
data: Default::default(),
};
submissions.save(self.round_submissions)?;
// oracle can start new round after `restart_delay` rounds
oracle.allow_start_round = self.round_id + (aggregator.config.restart_delay as u64);
@ -327,6 +360,8 @@ impl Processor {
rent: accounts.get_rent(0)?,
aggregator: accounts.get(1)?,
aggregator_owner: accounts.get(2)?,
round_submissions: accounts.get(3)?,
answer_submissions: accounts.get(4)?,
config,
}
.process(),
@ -355,8 +390,10 @@ impl Processor {
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_submissions: accounts.get(2)?,
answer_submissions: accounts.get(3)?,
oracle: accounts.get(4)?,
oracle_owner: accounts.get(5)?,
round_id,
value,
@ -382,8 +419,8 @@ impl Processor {
mod tests {
use super::*;
use crate::borsh_utils;
use crate::instruction;
use crate::{borsh_utils, state::Submission};
use borsh::BorshSerialize;
use solana_program::sysvar;
@ -473,7 +510,14 @@ mod tests {
}
}
fn create_aggregator(program_id: &Pubkey) -> Result<(TAccount, TAccount), ProgramError> {
struct TAggregator {
aggregator: TAccount,
aggregator_owner: TAccount,
round_submissions: TAccount,
answer_submissions: TAccount,
}
fn create_aggregator(program_id: &Pubkey) -> Result<TAggregator, ProgramError> {
let mut rent_sysvar = rent_sysvar();
let mut aggregator = TAccount::new_rent_exempt(
&program_id,
@ -481,6 +525,16 @@ mod tests {
false,
);
let mut aggregator_owner = TAccount::new(&program_id, true);
let mut round_submissions = TAccount::new_rent_exempt(
&program_id,
borsh_utils::get_packed_len::<Submissions>(),
false,
);
let mut answer_submissions = TAccount::new_rent_exempt(
&program_id,
borsh_utils::get_packed_len::<Submissions>(),
false,
);
process(
&program_id,
@ -500,11 +554,18 @@ mod tests {
(&mut rent_sysvar).into(),
(&mut aggregator).into(),
(&mut aggregator_owner).into(),
(&mut round_submissions).into(),
(&mut answer_submissions).into(),
]
.as_slice(),
)?;
Ok((aggregator, aggregator_owner))
Ok(TAggregator {
aggregator,
aggregator_owner,
round_submissions,
answer_submissions,
})
}
fn create_oracle(
@ -545,7 +606,11 @@ mod tests {
#[test]
fn test_configure() -> ProgramResult {
let program_id = Pubkey::new_unique();
let (mut aggregator, mut aggregator_owner) = create_aggregator(&program_id)?;
let TAggregator {
mut aggregator,
mut aggregator_owner,
..
} = create_aggregator(&program_id)?;
process(
&program_id,
@ -568,7 +633,11 @@ mod tests {
fn test_add_and_remove_oracle() -> ProgramResult {
let program_id = Pubkey::new_unique();
let (mut aggregator, mut aggregator_owner) = create_aggregator(&program_id)?;
let TAggregator {
mut aggregator,
mut aggregator_owner,
..
} = create_aggregator(&program_id)?;
let (mut oracle, _oracle_owner) =
create_oracle(&program_id, &mut aggregator, &mut aggregator_owner)?;
@ -589,7 +658,7 @@ mod tests {
struct SubmitTestFixture {
program_id: Pubkey,
aggregator: TAccount,
t_aggregator: TAggregator,
}
impl SubmitTestFixture {
@ -608,38 +677,66 @@ mod tests {
instruction::Instruction::Submit { round_id, value },
vec![
(&mut clock).into(),
self.aggregator.info(),
self.t_aggregator.aggregator.info(),
self.t_aggregator.round_submissions.info(),
self.t_aggregator.answer_submissions.info(),
oracle.into(),
oracle_owner.into(),
]
.as_slice(),
)?;
Aggregator::load_initialized(&self.aggregator.info())
Aggregator::load_initialized(&self.t_aggregator.aggregator.info())
}
fn aggregator(&mut self) -> Result<Aggregator, ProgramError> {
Aggregator::load_initialized(&self.t_aggregator.aggregator.info())
}
fn create_oracle(&mut self) -> Result<(TAccount, TAccount), ProgramError> {
create_oracle(
&self.program_id,
&mut self.t_aggregator.aggregator,
&mut self.t_aggregator.aggregator_owner,
)
}
fn round_submission(&mut self, i: usize) -> Result<Submission, ProgramError> {
Ok(self.round_submissions()?.data[i])
}
fn round_submissions(&mut self) -> Result<Submissions, ProgramError> {
self.aggregator()?
.round_submissions(&self.t_aggregator.round_submissions.info())
}
fn answer_submission(&mut self, i: usize) -> Result<Submission, ProgramError> {
Ok(self.answer_submissions()?.data[i])
}
fn answer_submissions(&mut self) -> Result<Submissions, ProgramError> {
self.aggregator()?
.answer_submissions(&self.t_aggregator.answer_submissions.info())
}
}
#[test]
fn test_submit() -> ProgramResult {
let program_id = Pubkey::new_unique();
let (mut aggregator, mut aggregator_owner) = create_aggregator(&program_id)?;
let (mut oracle, mut oracle_owner) =
create_oracle(&program_id, &mut aggregator, &mut aggregator_owner)?;
let (mut oracle2, mut oracle_owner2) =
create_oracle(&program_id, &mut aggregator, &mut aggregator_owner)?;
let (mut oracle3, mut oracle_owner3) =
create_oracle(&program_id, &mut aggregator, &mut aggregator_owner)?;
let mut fixture = SubmitTestFixture {
let mut tt = SubmitTestFixture {
program_id,
aggregator,
t_aggregator: create_aggregator(&program_id)?,
};
let (mut oracle, mut oracle_owner) = tt.create_oracle()?;
let (mut oracle2, mut oracle_owner2) = tt.create_oracle()?;
let (mut oracle3, mut oracle_owner3) = tt.create_oracle()?;
let time = 100;
let agr = fixture.submit(&mut oracle, &mut oracle_owner, time, 0, 1)?;
let agr = tt.submit(&mut oracle, &mut oracle_owner, time, 0, 1)?;
let oracle_state = Oracle::load_initialized(&oracle.info())?;
let sub = &agr.current_round.submissions[0];
let round = &agr.current_round;
let sub = tt.round_submission(0)?;
let round = &agr.round;
assert_eq!(oracle_state.withdrawable, 10);
assert_eq!(round.created_at, time);
assert_eq!(round.updated_at, time);
@ -650,8 +747,7 @@ mod tests {
// test: should fail with repeated submission
assert_eq!(
fixture
.submit(&mut oracle, &mut oracle_owner, time + 10, 0, 2)
tt.submit(&mut oracle, &mut oracle_owner, time + 10, 0, 2)
.map_err(Error::from),
Err(Error::OracleAlreadySubmitted),
"should fail if oracle submits repeatedly in the same round"
@ -659,10 +755,10 @@ mod tests {
let old_time = time;
let time = 200;
let agr = fixture.submit(&mut oracle2, &mut oracle_owner2, time, 0, 2)?;
let agr = tt.submit(&mut oracle2, &mut oracle_owner2, time, 0, 2)?;
let oracle_state = Oracle::load_initialized(&oracle.info())?;
let sub = &agr.current_round.submissions[1];
let round = &agr.current_round;
let sub = tt.round_submission(1)?;
let round = &agr.round;
assert_eq!(oracle_state.withdrawable, 10);
assert_eq!(round.created_at, old_time);
assert_eq!(round.updated_at, time);
@ -670,28 +766,29 @@ mod tests {
assert_eq!(sub.value, 2);
assert_eq!(sub.updated_at, time);
// test: answer resolved when min_submissions is reached
// // test: answer resolved when min_submissions is reached
let answer = &agr.answer;
assert_eq!(answer.is_initialized(), true);
assert_eq!(answer.updated_at, time);
assert_eq!(answer.created_at, time);
assert_eq!(answer.submissions, agr.current_round.submissions);
let answer_submissions = tt.answer_submissions()?;
let round_submissions = tt.round_submissions()?;
assert_eq!(answer_submissions, round_submissions);
// test: max submission reached
assert_eq!(
fixture
.submit(&mut oracle3, &mut oracle_owner3, time + 10, 0, 2)
tt.submit(&mut oracle3, &mut oracle_owner3, time + 10, 0, 2)
.map_err(Error::from),
Err(Error::MaxSubmissionsReached),
);
// test: start new round
let time = 300;
let agr = fixture.submit(&mut oracle, &mut oracle_owner, time, 1, 10)?;
let agr = tt.submit(&mut oracle, &mut oracle_owner, time, 1, 10)?;
let oracle_state = Oracle::load_initialized(&oracle.info())?;
let sub = &agr.current_round.submissions[0];
let round = &agr.current_round;
let sub = tt.round_submission(0)?;
let round = &agr.round;
assert_eq!(oracle_state.withdrawable, 20);
assert_eq!(round.id, 1);
assert_eq!(round.created_at, time);
@ -700,7 +797,7 @@ mod tests {
assert_eq!(sub.value, 10);
assert_eq!(sub.updated_at, time);
assert_eq!(
round.submissions[1].is_initialized(),
tt.round_submission(1)?.is_initialized(),
false,
"other submissions should've been zero after starting a new round"
);
@ -714,16 +811,15 @@ mod tests {
// test: oracle cannot immediately start a new round
assert_eq!(
fixture
.submit(&mut oracle, &mut oracle_owner, time + 10, 2, 2)
tt.submit(&mut oracle, &mut oracle_owner, time + 10, 2, 2)
.map_err(Error::from),
Err(Error::OracleNewRoundCooldown),
);
// test: resolve a new round
let time = 400;
let agr = fixture.submit(&mut oracle2, &mut oracle_owner2, time, 1, 20)?;
let sub = &agr.current_round.submissions[1];
let agr = tt.submit(&mut oracle2, &mut oracle_owner2, time, 1, 20)?;
let sub = tt.round_submission(1)?;
assert_eq!(sub.oracle, oracle2.pubkey.to_bytes());
assert_eq!(sub.value, 20);
@ -734,35 +830,34 @@ mod tests {
assert_eq!(answer.round_id, 1);
assert_eq!(answer.updated_at, time);
assert_eq!(answer.created_at, time);
assert_eq!(answer.submissions[0].value, 10);
assert_eq!(answer.submissions[1].value, 20);
assert_eq!(tt.answer_submission(0)?.value, 10);
assert_eq!(tt.answer_submission(1)?.value, 20);
let time = 500;
// let oracle 2 start a new round
let agr = fixture.submit(&mut oracle2, &mut oracle_owner2, time, 2, 200)?;
let round = &agr.current_round;
let agr = tt.submit(&mut oracle2, &mut oracle_owner2, time, 2, 200)?;
let round = &agr.round;
assert_eq!(round.id, 2);
let agr = fixture.submit(&mut oracle, &mut oracle_owner, time, 3, 200)?;
let round = &agr.current_round;
let agr = tt.submit(&mut oracle, &mut oracle_owner, time, 3, 200)?;
let round = &agr.round;
assert_eq!(round.id, 3);
let agr = fixture.submit(&mut oracle2, &mut oracle_owner2, time, 4, 200)?;
let round = &agr.current_round;
let agr = tt.submit(&mut oracle2, &mut oracle_owner2, time, 4, 200)?;
let round = &agr.round;
assert_eq!(round.id, 4);
// InvalidRoundID
assert_eq!(
fixture
.submit(&mut oracle, &mut oracle_owner, time + 10, 10, 1000)
tt.submit(&mut oracle, &mut oracle_owner, time + 10, 10, 1000)
.map_err(Error::from),
Err(Error::InvalidRoundID),
"should only be able to start a round with current_round.id + 1"
);
assert_eq!(
fixture
.submit(&mut oracle3, &mut oracle_owner3, time + 10, 3, 1000)
tt.submit(&mut oracle3, &mut oracle_owner3, time + 10, 3, 1000)
.map_err(Error::from),
Err(Error::InvalidRoundID),
"should not be able to submit answer to previous rounds"

View File

@ -13,10 +13,16 @@ use solana_program::{
};
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
pub struct Pubkey([u8; 32]);
pub struct PublicKey(pub [u8; 32]);
impl<'a> From<&'a AccountInfo<'a>> for PublicKey {
fn from(info: &'a AccountInfo<'a>) -> Self {
PublicKey(info.key.to_bytes())
}
}
pub trait Authority {
fn authority(&self) -> Pubkey;
fn authority(&self) -> &PublicKey;
fn authorize(&self, account: &AccountInfo) -> ProgramResult {
if !account.is_signer {
@ -52,19 +58,34 @@ pub struct AggregatorConfig {
pub reward_amount: u64,
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
pub struct Submissions {
pub is_initialized: bool,
// can we try using vector to avoid using the stack?
pub data: [Submission; MAX_ORACLES],
// pub data: Vec<Submission>,
}
impl IsInitialized for Submissions {
fn is_initialized(&self) -> bool {
self.is_initialized
}
}
impl BorshState for Submissions {}
impl InitBorshState for Submissions {}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
pub struct Round {
pub id: u64,
pub created_at: u64,
pub updated_at: u64,
pub submissions: [Submission; MAX_ORACLES],
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
pub struct Answer {
pub round_id: u64,
pub created_at: u64,
pub updated_at: u64,
pub submissions: [Submission; MAX_ORACLES],
}
impl IsInitialized for Answer {
@ -80,16 +101,35 @@ pub struct Aggregator {
/// is initialized
pub is_initialized: bool,
/// authority
pub owner: [u8; 32],
pub owner: PublicKey,
/// current round accepting oracle submissions
pub current_round: Round,
pub round: Round,
pub round_submissions: PublicKey, // has_one: Submissions
/// the latest answer resolved
pub answer: Answer,
pub answer_submissions: PublicKey, // has_one: Submissions
}
impl Aggregator {
/// check & return the submissions linked with an aggregator
pub fn answer_submissions(&self, account: &AccountInfo) -> Result<Submissions, ProgramError> {
if self.answer_submissions.0 != account.key.to_bytes() {
Err(Error::AggregatorMismatch)?;
}
Submissions::load_initialized(account)
}
pub fn round_submissions(&self, account: &AccountInfo) -> Result<Submissions, ProgramError> {
if self.round_submissions.0 != account.key.to_bytes() {
Err(Error::AggregatorMismatch)?;
}
Submissions::load_initialized(account)
}
}
impl Authority for Aggregator {
fn authority(&self) -> Pubkey {
Pubkey(self.owner)
fn authority(&self) -> &PublicKey {
&self.owner
}
}
impl IsInitialized for Aggregator {
@ -131,13 +171,13 @@ pub struct Oracle {
pub allow_start_round: u64,
/// aggregator
pub aggregator: [u8; 32],
pub aggregator: PublicKey,
/// owner
pub owner: [u8; 32],
pub owner: PublicKey,
}
impl Authority for Oracle {
fn authority(&self) -> Pubkey {
Pubkey(self.owner)
fn authority(&self) -> &PublicKey {
&self.owner
}
}
impl BorshState for Oracle {}