diff --git a/docker/Dockerfile b/docker/Dockerfile index fb7394d90..62e3dec84 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -27,8 +27,9 @@ RUN apt-get install -qq \ qt5-qmake \ qtbase5-dev-tools \ libqt5websockets5-dev\ - libclang-dev - + libclang-dev\ + python3-pip + # Install jcon-cpp library RUN git clone https://github.com/joncol/jcon-cpp.git /jcon-cpp && cd /jcon-cpp && git checkout 2235654e39c7af505d7158bf996e47e37a23d6e3 && mkdir build && cd build && cmake .. && make -j4 && make install @@ -56,6 +57,8 @@ export PYTHONPATH=\"\${PYTHONPATH:+\$PYTHONPATH:}\${HOME}/pyth-client\"\n\ COPY --chown=pyth:pyth . pyth-client/ +RUN pip3 install solana + # Build off-chain binaries. RUN cd pyth-client && ./scripts/build.sh diff --git a/program/c/src/oracle/oracle.h b/program/c/src/oracle/oracle.h index a85e0632c..3db9b8b09 100644 --- a/program/c/src/oracle/oracle.h +++ b/program/c/src/oracle/oracle.h @@ -270,11 +270,11 @@ typedef enum { // key[2] sysvar_clock account [readable] e_cmd_upd_price_no_fail_on_error, - // performs migation logic on the upgraded account. (resizes price accounts) + // resizes a price account so that it fits the Time Machine // key[0] funding account [signer writable] - // key[1] upgraded account [writable] + // key[1] price account [Signer writable] // key[2] system program [readable] - e_cmd_upd_account_version, + e_cmd_resize_price_account, } command_t; typedef struct cmd_hdr diff --git a/program/rust/src/log.rs b/program/rust/src/log.rs index 00fbf4b88..3128419f8 100644 --- a/program/rust/src/log.rs +++ b/program/rust/src/log.rs @@ -85,6 +85,11 @@ pub fn pre_log(accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResu command_t_e_cmd_upd_product => { msg!("UpdateProduct"); } + + command_t_e_cmd_resize_price_account => { + //accounts[1] is the updated account + msg!("ResizePriceAccount: {}", accounts[1].key); + } _ => { msg!("UnrecognizedInstruction"); return Err(OracleError::UnrecognizedInstruction.into()); diff --git a/program/rust/src/processor.rs b/program/rust/src/processor.rs index be052b9ef..fbed5f8a9 100644 --- a/program/rust/src/processor.rs +++ b/program/rust/src/processor.rs @@ -13,8 +13,8 @@ use crate::c_oracle_header::{ command_t_e_cmd_del_publisher, command_t_e_cmd_init_mapping, command_t_e_cmd_init_price, + command_t_e_cmd_resize_price_account, command_t_e_cmd_set_min_pub, - command_t_e_cmd_upd_account_version, command_t_e_cmd_upd_price, command_t_e_cmd_upd_price_no_fail_on_error, command_t_e_cmd_upd_product, @@ -33,10 +33,10 @@ use crate::rust_oracle::{ del_publisher, init_mapping, init_price, + resize_price_account, set_min_pub, upd_product, update_price, - update_version, }; ///dispatch to the right instruction in the oracle @@ -63,8 +63,8 @@ pub fn process_instruction( command_t_e_cmd_upd_price | command_t_e_cmd_upd_price_no_fail_on_error | command_t_e_cmd_agg_price => update_price(program_id, accounts, instruction_data, input), - command_t_e_cmd_upd_account_version => { - update_version(program_id, accounts, instruction_data) + command_t_e_cmd_resize_price_account => { + resize_price_account(program_id, accounts, instruction_data) } command_t_e_cmd_add_price => add_price(program_id, accounts, instruction_data), command_t_e_cmd_init_mapping => init_mapping(program_id, accounts, instruction_data), diff --git a/program/rust/src/rust_oracle.rs b/program/rust/src/rust_oracle.rs index 68986d08f..cde95c017 100644 --- a/program/rust/src/rust_oracle.rs +++ b/program/rust/src/rust_oracle.rs @@ -19,6 +19,13 @@ use solana_program::program_memory::{ use solana_program::pubkey::Pubkey; use solana_program::rent::Rent; + +use crate::time_machine_types::PriceAccountWrapper; +use solana_program::program::invoke; +use solana_program::system_instruction::transfer; +use solana_program::system_program::check_id; + + use crate::c_oracle_header::{ cmd_add_price_t, cmd_add_publisher_t, @@ -42,6 +49,8 @@ use crate::c_oracle_header::{ PC_MAX_NUM_DECIMALS, PC_PROD_ACC_SIZE, PC_PTYPE_UNKNOWN, + PC_VERSION, + SUCCESSFULLY_UPDATED_AGGREGATE, }; use crate::deserialize::{ load, @@ -49,34 +58,112 @@ use crate::deserialize::{ load_account_as_mut, }; use crate::error::OracleResult; -use crate::utils::pyth_assert; use crate::OracleError; +use crate::utils::pyth_assert; + use super::c_entrypoint_wrapper; +const PRICE_T_SIZE: usize = size_of::(); +const PRICE_ACCOUNT_SIZE: usize = size_of::(); + ///Calls the c oracle update_price, and updates the Time Machine if needed pub fn update_price( _program_id: &Pubkey, - _accounts: &[AccountInfo], + accounts: &[AccountInfo], _instruction_data: &[u8], input: *mut u8, ) -> OracleResult { - //For now, we did not change the behavior of this. this is just to show the proposed structure - // of the program - c_entrypoint_wrapper(input) + let c_ret_value = c_entrypoint_wrapper(input)?; + let price_account_info = &accounts[1]; + //accounts checks happen in c_entrypoint + let account_len = price_account_info.try_data_len()?; + match account_len { + PRICE_T_SIZE => Ok(c_ret_value), + PRICE_ACCOUNT_SIZE => { + if c_ret_value == SUCCESSFULLY_UPDATED_AGGREGATE { + let mut price_account = + load_account_as_mut::(price_account_info)?; + price_account.add_price_to_time_machine()?; + } + Ok(c_ret_value) + } + _ => Err(ProgramError::InvalidArgument), + } } -/// has version number/ account type dependant logic to make sure the given account is compatible -/// with the current version -/// updates the version number for all accounts, and resizes price accounts -pub fn update_version( - _program_id: &Pubkey, - _accounts: &[AccountInfo], +fn send_lamports<'a>( + from: &AccountInfo<'a>, + to: &AccountInfo<'a>, + system_program: &AccountInfo<'a>, + amount: u64, +) -> Result<(), ProgramError> { + let transfer_instruction = transfer(from.key, to.key, amount); + invoke( + &transfer_instruction, + &[from.clone(), to.clone(), system_program.clone()], + )?; + Ok(()) +} + +/// resizes a price account so that it fits the Time Machine +/// key[0] funding account [signer writable] +/// key[1] price account [Signer writable] +/// key[2] system program [readable] +pub fn resize_price_account( + program_id: &Pubkey, + accounts: &[AccountInfo], _instruction_data: &[u8], ) -> OracleResult { - panic!("Need to merge fix to pythd in order to implement this"); - // Ok(SUCCESS) + let [funding_account_info, price_account_info, system_program] = match accounts { + [x, y, z] => Ok([x, y, z]), + _ => Err(ProgramError::InvalidArgument), + }?; + + check_valid_funding_account(funding_account_info)?; + check_valid_signable_account(program_id, price_account_info, size_of::())?; + pyth_assert( + check_id(system_program.key), + OracleError::InvalidSystemAccount.into(), + )?; + //throw an error if not a price account + //need to makre sure it goes out of scope immediatly to avoid mutable borrow errors + { + load_checked::(price_account_info, PC_VERSION)?; + } + let account_len = price_account_info.try_data_len()?; + match account_len { + PRICE_T_SIZE => { + //ensure account is still rent exempt after resizing + let rent: Rent = Default::default(); + let lamports_needed: u64 = rent + .minimum_balance(size_of::()) + .saturating_sub(price_account_info.lamports()); + if lamports_needed > 0 { + send_lamports( + funding_account_info, + price_account_info, + system_program, + lamports_needed, + )?; + } + //resize + //we do not need to zero initialize since this is the first time this memory + //is allocated + price_account_info.realloc(size_of::(), false)?; + //The load below would fail if the account was not a price account, reverting the whole + // transaction + let mut price_account = + load_checked::(price_account_info, PC_VERSION)?; + //Initialize Time Machine + price_account.initialize_time_machine()?; + Ok(SUCCESS) + } + PRICE_ACCOUNT_SIZE => Ok(SUCCESS), + _ => Err(ProgramError::InvalidArgument), + } } + /// initialize the first mapping account in a new linked-list of mapping accounts /// accounts[0] funding account [signer writable] /// accounts[1] new mapping account [signer writable] @@ -150,6 +237,7 @@ pub fn add_price( ProgramError::InvalidArgument, )?; + let [funding_account, product_account, price_account] = match accounts { [x, y, z] => Ok([x, y, z]), _ => Err(ProgramError::InvalidArgument), diff --git a/program/rust/src/time_machine_types.rs b/program/rust/src/time_machine_types.rs index 8298f6baa..bccc1e0c3 100644 --- a/program/rust/src/time_machine_types.rs +++ b/program/rust/src/time_machine_types.rs @@ -1,4 +1,19 @@ -#[derive(Debug, Clone)] +use crate::c_oracle_header::{ + pc_price_t, + PythAccount, + EXTRA_PUBLISHER_SPACE, + PC_ACCTYPE_PRICE, + PC_PRICE_T_COMP_OFFSET, +}; +use crate::error::OracleError; +use bytemuck::{ + Pod, + Zeroable, +}; +use solana_program::msg; + + +#[derive(Debug, Clone, Copy)] #[repr(C)] /// this wraps multiple SMA and tick trackers, and includes all the state /// used by the time machine @@ -7,10 +22,52 @@ pub struct TimeMachineWrapper { place_holder: [u8; 1864], } +#[derive(Copy, Clone)] +#[repr(C)] +/// wraps everything stored in a price account +pub struct PriceAccountWrapper { + //an instance of the c price_t type + pub price_data: pc_price_t, + //space for more publishers + pub extra_publisher_space: [u8; EXTRA_PUBLISHER_SPACE as usize], + //TimeMachine + pub time_machine: TimeMachineWrapper, +} +impl PriceAccountWrapper { + pub fn initialize_time_machine(&mut self) -> Result<(), OracleError> { + msg!("implement me"); + Ok(()) + } + + pub fn add_price_to_time_machine(&mut self) -> Result<(), OracleError> { + msg!("implement me"); + Ok(()) + } +} + +#[cfg(target_endian = "little")] +unsafe impl Zeroable for PriceAccountWrapper { +} + +#[cfg(target_endian = "little")] +unsafe impl Pod for PriceAccountWrapper { +} + +impl PythAccount for PriceAccountWrapper { + const ACCOUNT_TYPE: u32 = PC_ACCTYPE_PRICE; + const INITIAL_SIZE: u32 = PC_PRICE_T_COMP_OFFSET as u32; +} + #[cfg(test)] pub mod tests { - use crate::c_oracle_header::TIME_MACHINE_STRUCT_SIZE; - use crate::time_machine_types::TimeMachineWrapper; + use crate::c_oracle_header::{ + PRICE_ACCOUNT_SIZE, + TIME_MACHINE_STRUCT_SIZE, + }; + use crate::time_machine_types::{ + PriceAccountWrapper, + TimeMachineWrapper, + }; use std::mem::size_of; #[test] ///test that the size defined in C matches that @@ -24,4 +81,15 @@ pub mod tests { size_of::() ); } + #[test] + ///test that priceAccountWrapper has a correct size + fn c_price_account_size_is_correct() { + assert_eq!( + size_of::(), + PRICE_ACCOUNT_SIZE.try_into().unwrap(), + "expected PRICE_ACCOUNT_SIZE ({}) in oracle.h to the same as the size of PriceAccountWrapper ({})", + PRICE_ACCOUNT_SIZE, + size_of::() + ); + } } diff --git a/pyth/tests/test_publish.py b/pyth/tests/test_publish.py index f9e8bde6c..c33a6caf3 100644 --- a/pyth/tests/test_publish.py +++ b/pyth/tests/test_publish.py @@ -1,10 +1,20 @@ +from typing import Dict +from construct import Bytes, Int32sl, Int32ul, Struct +from solana.publickey import PublicKey +from solana.transaction import AccountMeta, TransactionInstruction, Transaction +from solana.keypair import Keypair +from solana.rpc.api import Client +import base64 +import os +import asyncio + import json import time from subprocess import check_call, check_output def test_publish(solana_test_validator, pyth_dir, - pyth_init_product, pyth_init_price): + pyth_init_product, pyth_init_price, solana_keygen, solana_program_deploy): def get_price_acct(): cmd = [ @@ -19,6 +29,59 @@ def get_price_acct(): output = output.decode('ascii') output = json.loads(output) return output['price_accounts'][0] + + def get_key_pair(path_to_file): + with open(path_to_file) as key_file: + key_data = json.load(key_file) + return Keypair.from_secret_key(key_data) + + def get_path_to_pythdir_pair(account_addr): + return os.path.join(pyth_dir, "account_" + str(account_addr) + ".json") + + + + def resize_account(price_account_address): + """ + given a string with the pubkey of a price account it calls the resize instruction of the Oracle on it + """ + #constants from oracle.h + PROGRAM_VERSION = 2 #TODO: update this + COMMAND_UPD_ACCOUNT = 14 + SYSTEM_PROGRAM = "11111111111111111111111111111111" + + #update version of price accounts to make sure they resize + layout = Struct("version" / Int32ul, "command" / Int32sl) + data = layout.build(dict(version=PROGRAM_VERSION, command=COMMAND_UPD_ACCOUNT)) + funding_key = PublicKey(solana_keygen[0]) + price_key = PublicKey(price_account_address) + system_key = PublicKey(SYSTEM_PROGRAM) + resize_instruction = TransactionInstruction( + data=data, + keys=[ + AccountMeta(pubkey=funding_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=price_key, is_signer=True, is_writable=True), + AccountMeta(pubkey=system_key, is_signer=False, is_writable=False), + ], + program_id = PublicKey(solana_program_deploy), + ) + txn = Transaction().add(resize_instruction) + txn.fee_payer = funding_key + funding_key_pair = get_key_pair(solana_keygen[1]) + path_to_price = get_path_to_pythdir_pair(price_key) + price_key_pair = get_key_pair(path_to_price) + solana_client = Client("http://localhost:8899") + solana_client.send_transaction(txn, funding_key_pair, price_key_pair) + + def get_account_size(acc_address): + """ + given a string with the pubkey of an account, return its size + """ + PublicKey(acc_address) + solana_client = Client("http://localhost:8899") + data = solana_client.get_account_info(PublicKey(acc_address), encoding = 'base64')['result']['value']['data'][0] + data = base64.b64decode(data) + return len(data) + before = get_price_acct() assert before['publisher_accounts'][0]['price'] == 0 @@ -49,8 +112,46 @@ def get_price_acct(): check_call(cmd) time.sleep(20) - after = get_price_acct() assert after['publisher_accounts'][0]['price'] == 150 assert after['publisher_accounts'][0]['conf'] == 7 assert after['publisher_accounts'][0]['status'] == 'trading' + + resize_account(pyth_init_price['LTC']) + time.sleep(20) + #defined in oracle.h + new_account_size = 6176 + assert get_account_size(pyth_init_price['LTC']) == new_account_size + + + + #try adding a new price to the resized accounts + cmd = [ + 'pyth', 'upd_price_val', + pyth_init_price['LTC'], + '100', '1', 'trading', + '-r', 'localhost', + '-k', pyth_dir, + '-c', 'finalized', + '-x', + ] + check_call(cmd) + + time.sleep(20) + + cmd = [ + 'pyth', 'upd_price', + pyth_init_price['LTC'], + '-r', 'localhost', + '-k', pyth_dir, + '-c', 'finalized', + '-x', + ] + check_call(cmd) + + time.sleep(20) + + after = get_price_acct() + assert after['publisher_accounts'][0]['price'] == 100 + assert after['publisher_accounts'][0]['conf'] == 1 + assert after['publisher_accounts'][0]['status'] == 'trading' diff --git a/scripts/build-bpf.sh b/scripts/build-bpf.sh index 2ed8be054..15aaf503f 100755 --- a/scripts/build-bpf.sh +++ b/scripts/build-bpf.sh @@ -46,4 +46,3 @@ sha256sum ./target/**/*.so -