Skip to content

Conversation

swimricky
Copy link
Contributor

@swimricky swimricky commented Apr 3, 2023

Summary

Implement mock-cpi-program::update_price & accumuulator-updater::update_inputs ixs

Updates

  • refactor state & ixs in both programs into their own modules
  • implement update_price & update_inputs ixs

TODOs

  • combine create_inputs & update_inputs ix into one (see Notes below)
  • Rust client sdk/examples
  • delete_inputs (See Notes below)
  • update invoke_cpi_solana to use instruction::new_with_borsh for instruction_data serialization

Testing

cd accumulator_updater && anchor test

Notes

  • for delete_inputs do we need to handle deleting inputs outside of a CPI call? one use-case would be if an accumulator message schema was being deprecated/no longer used

…rogram

Initial layout for accumulator updater program. Includes mock-cpi-caller which is meant to represent
pyth oracle(or any future allowed program) that will call the accumulator updater program. All
implementation details are open for discussion/subject to change
# Conflicts:
#	accumulator_updater/Cargo.lock
#	accumulator_updater/programs/accumulator_updater/src/lib.rs
#	accumulator_updater/programs/mock-cpi-caller/Cargo.toml
#	accumulator_updater/programs/mock-cpi-caller/src/lib.rs
#	accumulator_updater/tests/accumulator_updater.ts
rename schema to message & associated price messages, remove unncessary
comments, changed addAllowedProgram to setAllowedPrograms
consolidate SetAllowedPrograms and UpdateWhitelistAuthority into one context
@vercel
Copy link

vercel bot commented Apr 3, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Ignored Deployments
Name Status Preview Comments Updated (UTC)
example-oracle-amm ⬜️ Ignored (Inspect) Apr 5, 2023 11:26pm
xc-admin-frontend ⬜️ Ignored (Inspect) Apr 5, 2023 11:26pm

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor only

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new update_input logic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor of AccumulatorInput & AccumulatorHeader account state

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor of whitelist related logic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor of mock add_price

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new mock update_price logic

Ok(())
}

pub(crate) fn update(&mut self, params: UpdatePriceParams) -> Result<()> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new update fn

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor besides PriceAccount.update()

Copy link
Contributor

@jayantk jayantk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice! I left you a bunch of minor comments and questions, but overall seems like a step in the right direction.

base_account: Pubkey,
data: Vec<Vec<u8>>,
account_type: u32,
account_schemas: Vec<u8>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice if the interface of this function resembled a commonly-used data structure so that callers had a natural reference point for what to expect. I think the accumulator is most similar to a Map where the keys are (base_account, account_schema) and the values are arbitrary data. Given that, I would try to make the arguments map on to the function signature for inserting multiple items in a Map. E.g., I think a function signature like

/// Insert elements into the Map identified by the calling program and map_id.
/// Each element is written into a separate account (explain how to derive the PDA). 
/// The caller of this instruction must pass those PDAs while calling this function in the same order as elements. 
put_all(ctx, map_id: Pubkey, elements: <Vec<(u8, Vec<u8>)>>)

would make more sense. (map_id is base_account)

ctx: Context<'_, '_, '_, 'info, EmitInputs<'info>>,
base_account: Pubkey,
data: Vec<Vec<u8>>,
account_type: u32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need account_type at all? I don't remember exactly but i'm looking back at our previous convo and it's not mentioned there:

#712 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was for flexibility if we ever wanted to include different types of accounts into the accumulator aside from the price accounts - maybe even if it was something outside of pyth (i don't have any specific use-case in mind and not sure if we would ever want product or mapping accounts but I can remove this if we really don't need this)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove for now

let accumulator_input = AccumulatorInput::new(
AccumulatorHeader::new(
bump,
1, //from CPI caller?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this program controls the header format (and therefore gets to set the version)

I would suggest extracting this value out to a constant like AccumulatorHeader::CURRENT_VERSION though so it's easy to bump all the places where it may be used.


//TODO: update header?
accumulator_input.data = account_data;
accumulator_input.persist(ai)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the structure of this code should be:

phase 1: check that you have the right account key
phase 2: generate the accumulator_input and serialize it to a byte array
phase 3: given the byte array, write it to a new account or overwrite the existing account data.

that implementation is less error prone than this one -- in this version, we can easily forget to change one of the code paths for serialization and that will cause problems.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw i'm pretty sure we have code somewhere for doing the create/update w/ resizing if needed in some solana program somewhere. @drozdziak1 or @guibescos will be able to point you to it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the resizing logic from the oracle but it's here pyth-network/pyth-client@90ba997#diff-6b3dac646ee82aa19e2da0df982aa54c40e2005422fa4fd5805d1b5e840d3ca5L86 . It's pretty straightforward with realloc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will handle the re-sizing in a separate PR

pub payer: Signer<'info>,
pub whitelist_verifier: WhitelistVerifier<'info>,
pub system_program: Program<'info, System>,
//TODO: decide on using optional accounts vs ctx.remaining_accounts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like the implementation using ctx.remaining_accounts above is pretty reasonable. i would go with that.

@@ -18,6 +15,8 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
pub mod accumulator_updater {
use super::*;


/// Initializes the whitelist and sets it's authority to the provided pubkey
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add Once initialized, the authority must sign all further changes to the whitelist.

pub bump: u8,
pub version: u8,
// u32 for parity with pyth oracle contract
pub account_type: u32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we definitely don't need this -- we only send data from price accounts to other blockchains anyway.

Self { header, data }
}

pub fn validate_account_info(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't seem like the right encapsulation of functionality -- it's a very specific function that can only be used in one place. I think a better function would be something like derive_pda_for(cpi_caller, base_account, account_schema) ?

accumulator_input,
accumulator_size,
&ctx.accounts.payer,
&[accumulator_acc_seeds_with_bump!(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm a little confused about the use of this function w/ the bump vs. the one w/o the bump. It seems like you want there to be a single PDA for each key, right? if so, isn't the bump deterministically generated, so there shouldn't be a reason to save it? (is this an efficiency improvement? if so, does it not introduce security holes because there are now multiple PDAs that can pass the checks for the same key?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's the "canonical bump" which is the first bump counting down from 255 that provides a valid PDA address (off the ed25519 curve). This is the bump that's returned with find_program_address. But there could technically be multiple bumps that create valid PDA addresses.

There's 2 ways to verify a PDA address:

  1. assume you're always using the canonical bump and check that Pubkey::find_program_address(&[seeds]).0 == account.key()
  2. store the bump in the account and checkaccount.key() == Pubkey::create_program_address(&[seeds, &[account.bump]])

2 is more efficient (marginally) since 1 could theoretically have to do a guess-and-check multiple times to find the first valid seed.

Regarding your concern about security holes - since only a program can create PDAs it owns, it shouldn't be possible to have multiple PDAs using the same seeds but different bumps. The accumulator-updater program will be the only program to create these PDAs and it's using the canonical bumps when the PDAs are first initialized.

Having 2 macros is a bit confusing so i'll remove the non-bump one since that's only ever used once

// 44444 compute units
// AddPrice::invoke_cpi_anchor(ctx, account_data, PythAccountType::Price, account_schemas)
// 44045 compute units
AddPrice::emit_accumulator_inputs(ctx, account_data, PythAccountType::Price, account_schemas)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't necessary here, right? the update price instruction will also create the accounts now if they're missing, so we only have to call emit_inputs there?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I think this question may come down to a misunderstanding of how the oracle program works. In the oracle program, when you add a price account, it doesn't actually contain any price information. it simply allocates the space for putting prices in later. So there wouldn't even be any data to emit. (whereas this version of add price actually puts some data in the price account).

anyway, since this is just a test program, so I would scope it down to the minimum necessary code to demonstrate how to invoke the accumulators & test all the needed functionality. pretty sure you don't need both add price and update price to do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh got it thanks for clarifying that. i'll update this and the relevant tests/code and scope it down

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left the add_price & cpi call in for now but left a comment describing the actual behavior in the oracle program.

Remove account_type, rename emit_inputs to put_all to be more similar to a Map interface, added
AccumulatorHeader::CURRENT_VERSION
@swimricky swimricky merged commit 4502c6d into main Apr 5, 2023
@swimricky swimricky deleted the accumulator-updater/update-inputs-ix branch April 5, 2023 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants