Skip to content

Conversation

Riateche
Copy link
Contributor

@Riateche Riateche commented Aug 28, 2024

The new on-chain program allows publishers to push multiple price updates in a single instruction, reducing overall TPS.

Unresolved questions:

  • What's the good max number of prices in a publisher's buffer account?
  • How should we restrict access when creating a buffer account?

Copy link

vercel bot commented Aug 28, 2024

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

Name Status Preview Comments Updated (UTC)
api-reference ✅ Ready (Inspect) Visit Preview 💬 Add feedback Sep 9, 2024 9:52am
staking-v2 ❌ Failed (Inspect) Sep 9, 2024 9:52am
1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
xc-admin-frontend ⬜️ Ignored (Inspect) Visit Preview Sep 9, 2024 9:52am

pub struct PublisherPriceError;

impl PublisherPrice {
pub fn new(
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should not permit feed_index of 0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a check.

.ok_or(ProgramError::InvalidInstructionData)?;
ensure!(
ProgramError::InvalidInstructionData,
system_program::check_id(system.key)
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's use pubkey_eq function for this one too. check_id is not efficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

let vault = account
.cloned()
.ok_or(ProgramError::InvalidInstructionData)?;
let vault_pda = Pubkey::find_program_address(&[VAULT_SEED.as_bytes()], program_id);
Copy link
Collaborator

Choose a reason for hiding this comment

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

find_program_address is inefficient as might try 256 times with different seeds. Since this is constant based on our program id, we can use declare_id to define id and have this one as a static one with a test to verify that it matches.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced with create_program_address and added bumps to the instruction arguments.

let buffer = account
.cloned()
.ok_or(ProgramError::InvalidInstructionData)?;
let buffer_pda =
Copy link
Collaborator

Choose a reason for hiding this comment

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

same comment as above, i recommend either get bump u8 this as input instruction or put it in the account. then you can use create_program_address to only perform a single check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced with create_program_address and added bumps to the instruction arguments.


let size = publisher_prices::size(MAX_NUM_PRICES);
// Deposit enough tokens to allocate the account.
let lamports = (Rent::get()?).minimum_balance(size) * 2;
Copy link
Collaborator

Choose a reason for hiding this comment

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

why twice? for extra balance :?

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 don't know. Removed.

prices
.get_mut(start..end)
.ok_or(ExtendError::NotEnoughSpace)?
.copy_from_slice(new_prices);
Copy link
Collaborator

Choose a reason for hiding this comment

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

i get why we don't cast here and it is very good. to make this copy faster, let's use sol_memcpt. Also it's good to write this assumption that we don't do any input validation for the sake of speed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

if expected_len > prices_bytes.len() {
return Err(ReadAccountError::InvalidNumPrices);
}
let prices = cast_slice(&prices_bytes[..expected_len]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

please add a note here too that we don't do validation here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

*format == FORMAT
}

pub fn read(data: &[u8]) -> Result<(&PublisherPricesHeader, &[PublisherPrice]), ReadAccountError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

it's a bit strange that read and read_mut have different signatures.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a bit awkward because I want read to return only the initialized part of the account as &[PublisherPrice] slice, but in read_mut I need to return a full slice of the remainder of the account so that we can call extend later. To make it more clear that not all values returned by read_mut are properly initialized PublisherPrice values, I decided to return &mut [u8] instead of &mut [PublisherPrice]. I thought about encapsulating it in a new struct like this:

let mut extender = read_mut(data);
let publisher = extender.publisher();
extender.extend(new_prices);

But I decided to keep it simple for now. Let me know your thoughts.

#[cfg(test)]
mod tests {
#[tokio::test]
async fn test_submit_prices() {}
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe add some tests here too?

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've extended the tests a bit, see test_initialize_and_publish().

header.num_prices = 0;
}
publisher_prices::extend(header, prices, data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
Copy link
Collaborator

Choose a reason for hiding this comment

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

i think we can introduce some errors and then convert them to program error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored error handling to use impl From from custom errors to ProgramError.


#[derive(Debug, Clone, Copy, PartialEq, Eq, Zeroable, Pod)]
#[repr(C, packed)]
pub struct PublisherConfig {
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you add some docs on what is this account and how it's used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added some comments about accounts and instructions.

@@ -0,0 +1,156 @@
//! The PublisherPrices account acts as the buffer for storing prices sent
Copy link
Collaborator

Choose a reason for hiding this comment

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

please update 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.

Updated.

data: &[u8],
) -> ProgramResult {
match Instruction::parse(data) {
Ok(Instruction::Initialize) => initialize(program_id, accounts, &data[1..]),
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: it mightly be cleaner just to parse the data then pass it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. Refactored.

},
};

// TODO: restrict access?
Copy link
Collaborator

Choose a reason for hiding this comment

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

it's now access restricted right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. Updated.

},
};

pub fn validate_publisher<'a>(
Copy link
Collaborator

Choose a reason for hiding this comment

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

probably not important but as i'm not so optimist on bpf optimizer I am wondering whether we could avoid clone and use reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed clones.

buffer.key.to_bytes(),
)?;

// Write an initial Header into the buffer account to prepare it to receive prices.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want to have any minimum size check for the buffer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now we just check if the size is enough to fit the header. The code should work fine with any size.

Ok(payer)
}

fn pubkey_eq(a: &Pubkey, b: &Pubkey) -> bool {
Copy link
Collaborator

Choose a reason for hiding this comment

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

just for extra caution let's add inline here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

let publisher_config = publisher_config::read(*publisher_config_data)?;
ensure!(
ProgramError::InvalidArgument,
buffer.key.to_bytes() == publisher_config.buffer_account
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's use sol_memcmp here as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

let mut buffer_data = buffer.data.borrow_mut();
let (header, prices) = buffer::read_mut(*buffer_data)?;
let current_slot = Clock::get()?.slot;
if header.slot != current_slot {
Copy link
Collaborator

Choose a reason for hiding this comment

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

let's add a comment explaining the behaviour here a bit. i think it can be somewhat cleaner if we pass slot to buffer::extend and it handles that, we can then make sure the clearing works as expected by testing it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved, added a comment and a test.

Copy link
Collaborator

@ali-behjati ali-behjati left a comment

Choose a reason for hiding this comment

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

Nice!

@Riateche Riateche merged commit 2aef1ca into main Sep 9, 2024
5 of 6 checks passed
@Riateche Riateche deleted the add-publisher-program branch September 9, 2024 11:29

// Creates a config account that stores the authority pubkey.
// The authority is allowed to modify publisher configs.
pub fn initialize(
Copy link
Contributor

@jayantk jayantk Sep 9, 2024

Choose a reason for hiding this comment

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

Can you document the expected accounts for all of these instructions? It's hard to tell now without reading the method implementation carefully. Also please document any assumptions around those accounts (E.g., this is uninitialized and becomes initialized after)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in #1883.

Ok((header, prices))
}

pub fn extend(
Copy link
Contributor

Choose a reason for hiding this comment

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

sort of a nitpick but I think "extend" isn't a good name for this -- "extend" usually implies that you will add more data without overwriting existing data. I think "update" or "update_prices" would be a better name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in #1883.


// Creates a publisher config account and stores the buffer account pubkey in it.
// Verifies and initializes the buffer account.
pub fn initialize_publisher(
Copy link
Contributor

Choose a reason for hiding this comment

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

is it permissible for a publisher to call this instruction multiple times with different buffers? If not, what's the expected process for a publisher to resize their buffer if it is not big enough?

(treat this question as a request for documentation and put the answer in the code somewhere)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in #1883.

ensure!(ProgramError::InvalidArgument, publisher_config.is_writable);
}
ensure!(
ProgramError::MissingRequiredSignature,

Choose a reason for hiding this comment

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

This should probably be ProgramError::InvalidSeeds or ProgramError::InvalidArgument as above?

data: &mut [u8],
publisher: [u8; 32],
) -> Result<(&mut BufferHeader, &mut [u8]), ReadAccountError> {
if data.len() < size_of::<BufferHeader>() {

Choose a reason for hiding this comment

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

To avoid errors, wouldn't it make more sense to ensure that data can hold at least one price? I think a buffer that can only hold the header is pretty useless, as it will never be able to publish any prices (and in the current design such a mistake would be relatively bad because the buffer cannot be resized).

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.

5 participants