program: Add batch instruction#884
Conversation
|
@joncinque, I didn't add client code because the main purpose of the instruction seems to be for use with CPI, would it still be better to add it in as an instruction that takes raw bytes as arguments? |
|
Hey @joncinque, following up on this. |
|
Sorry for the late response, between Breakpoint and the holidays, we had a few other priorities unfortunately. @febo can you take a look? |
joncinque
left a comment
There was a problem hiding this comment.
Sorry for the slowness here, but looks great overall!
Just a few small points, and one huge point that someone (probably me) will need to go through all instructions to check for owner checks. Once that's done, we should be able to land this
| } | ||
|
|
||
| /// Deserialize the data for the Batch variant of the `TokenInstruction` | ||
| pub fn deserialize<'de, D>(d: D) -> Result<Vec<u8>, D::Error> |
There was a problem hiding this comment.
This return type made me go cross-eyed for a bit, since I've never seen a deserialize function that returns bytes, but in the context of TokenInstruction::Batch, it makes sense
| pub fn process_batch( | ||
| program_id: &Pubkey, | ||
| mut accounts: &[AccountInfo], | ||
| mut data: &[u8], | ||
| ) -> ProgramResult { |
There was a problem hiding this comment.
This looks correct, but as we have to do in p-token, we need to explicitly check ownership for any instructions that assume the runtime will do ownership checks at the end of the instruction: https://github.com/solana-program/token/blob/be42014b739dc8fbf7942f597d6ac736857896dd/pinocchio/program/src/processor/batch.rs#L49-L96
This will probably be a very painful process 🙃 We could add new cases for every successful instruction test, in which we hack the owner of all token accounts to another program, and see if the program returns an error, or if the runtime does.
If the runtime returns an error, we need to add an explicit ownership check.
Or we can just go one by one by hand and add check_program_account to all input accounts.
@febo any better ideas for how we can do this?
There was a problem hiding this comment.
I wasn't sure about why those checks were there, the main instructions seem to fail immediately upon write attempt, the ones that reach the end of execution and then fail are Close and Withdraw Excess Lamports.
I assumed that for cases where we are writing into account and the account did not belong to the program then it should at least be in the same state that it was at the start of execution of the batch instruction, though the drawback would be that an instruction would allow for accounts that don't belong to it's program to pass of as if they did in a transaction explorer(though Reallocate and Withdraw Excess Lamports currently seems to allow for this).
The changes i made were to add checks for each non-extension instruction(i.e instructions that don't have full fledged sub processors) that the runtime returns an error for and leave the non-extension instructions that already had checks, since the extensions are not used as frequently and most already had program owner checks i added checks for all the rest(i.e the group extension, metadata extension...), i can make re-adjustments after further review if need be.
The batch checks were added here and the tests here, the other changes are in the different processor files.
There was a problem hiding this comment.
Or we can just go one by one by hand and add
check_program_accountto all input accounts.
This seems a good approach.
|
Also, can you rebase your branch when you get the chance? |
joncinque
left a comment
There was a problem hiding this comment.
Thanks for addressing all the comments, this is really great work! I went over the whole program too, and confirm that all of the extension-related processor changes are good.
For the additional ownership checks, they're required with the batch instruction because it's possible to trick the token program otherwise.
For example, I can do initialize_account + + close_account, and the token program will happily write data to the new account, do things with it, and close it at the end of the batch instruction. With wrapped SOL accounts, this can become dangerous.
So with that in mind, let's add more explicit checks in processor.rs:
_process_initialize_mint(mint_info)_process_initialize_account(new_account_info / mint_info)_process_initialize_multisig(multisig_info)process_transfer(mint_info)process_approve(source_account_info / mint_info)process_revoke(source_account_info)process_set_authority(account_info)process_close_account(source_account_info)process_toggle_freeze_account(source_account_info, mint_info)process_initialize_mint_close_authority(mint_account_info)process_get_account_data_size(mint_account_info)process_initialize_immutable_owner(token_account_info)process_create_native_mint(native_mint_info)process_initialize_non_transferable_mint(mint_account_info)process_initialize_permanent_delegate(mint_account_info)process_withdraw_excess_lamports(source_info)process_unwrap_lamports(always check source_account_info)
joncinque
left a comment
There was a problem hiding this comment.
One last thing, then we can land this! Thanks for the quick turnaround on all these
| source_account.base.amount = remaining_amount.into(); | ||
| if source_account_info.key != destination_account_info.key { | ||
| let source_starting_lamports = source_account_info.lamports(); | ||
| **source_account_info.lamports.borrow_mut() = source_starting_lamports | ||
| .checked_sub(amount) | ||
| .ok_or(TokenError::Overflow)?; | ||
|
|
||
| let destination_starting_lamports = destination_account_info.lamports(); | ||
| **destination_account_info.lamports.borrow_mut() = destination_starting_lamports | ||
| .checked_add(amount) | ||
| .ok_or(TokenError::Overflow)?; | ||
| } | ||
| Ok(()) | ||
| let destination_starting_lamports = destination_account_info.lamports(); | ||
| **destination_account_info.lamports.borrow_mut() = destination_starting_lamports | ||
| .checked_add(amount) | ||
| .ok_or(TokenError::Overflow)?; | ||
| } |
There was a problem hiding this comment.
nit: This can now be put in a if amount !=0 { block
febo
left a comment
There was a problem hiding this comment.
Looks good – just have a question about the ordering of the owner checks.
There was a problem hiding this comment.
Are we ok with returning a different error? For example, a previous instruction that would fail to unpack the account data if an invalid account is passed, will now fail with an invalid owner.
There was a problem hiding this comment.
Changing errors shouldn't make a difference, since it's impossible to recover from errors in CPI
There was a problem hiding this comment.
But this will change errors for non-cpi calls as well, since the new checks are on the individual instructions.
There was a problem hiding this comment.
Sure, but what impact can that have?
| error, | ||
| TokenClientError::Client(Box::new(TransportError::TransactionError( | ||
| TransactionError::InstructionError(0, InstructionError::InvalidAccountData) | ||
| TransactionError::InstructionError(0, InstructionError::IncorrectProgramId) |
There was a problem hiding this comment.
This is an example of the change that I mentioned about the ownership checks. Probably it is ok to return a different error. Alternatively, we could move the owner checks to be the last statement of the processor. We would still get a different error if the account is not owned by the program, but other validation checks will stay the same.
There was a problem hiding this comment.
I see what you mean, but keeping the source code easier to read is more important in my opinion
|
Thanks for your contribution! This will be a huge help |
This PR adds the
batchinstruction to thetoken-2022program, implementation drawn from here and originally proposed here.The purpose is to allow the execution of multiple token instructions in a single program instruction – e.g., it is possible to execute multiple instructions in a single CPI call, which saves at least 1000 CUs (CPI base fee) for each additional instruction.
Scope
TokenInstructioninstruction variants(regular and POD) and associated testsTokenInstructionvariantIt closes #787.