-
Notifications
You must be signed in to change notification settings - Fork 301
Add publisher program #1854
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add publisher program #1854
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
1 Skipped Deployment
|
pub struct PublisherPriceError; | ||
|
||
impl PublisherPrice { | ||
pub fn new( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 :?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() {} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
2cda8ae
to
4df7172
Compare
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Zeroable, Pod)] | ||
#[repr(C, packed)] | ||
pub struct PublisherConfig { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
9db80bb
to
cc0cdcb
Compare
@@ -0,0 +1,156 @@ | |||
//! The PublisherPrices account acts as the buffer for storing prices sent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please update this
There was a problem hiding this comment.
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..]), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
…ion handlers, remove clones, fix feature
Ok(payer) | ||
} | ||
|
||
fn pubkey_eq(a: &Pubkey, b: &Pubkey) -> bool { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
|
||
// Creates a config account that stores the authority pubkey. | ||
// The authority is allowed to modify publisher configs. | ||
pub fn initialize( |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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>() { |
There was a problem hiding this comment.
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).
The new on-chain program allows publishers to push multiple price updates in a single instruction, reducing overall TPS.
Unresolved questions: