-
Notifications
You must be signed in to change notification settings - Fork 30
program: skip sysvars and stake config #87
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
Conversation
bfe5f39 to
59839ec
Compare
4f1629f to
2d53d1d
Compare
|
Given the future port to upstream BPF / Pinocchio, I'm wondering if it's still worth making this change. Since all of the Pinocchio deserializers are zero-copy, I believe it's cheaper to take in the sysvar by account rather using the syscall to get the data. We could give people the option -- if the account is present, deserialize from that, otherwise use the syscall. @febo do you have any thoughts about the best approach here? |
Agree, I think having the option of passing accounts is good. Anyone that needs to save transaction size can choose not to pass accounts and "pay" for the syscall; similarly, passing sysvar accounts save CUs (when using zero-copy) by avoiding the syscall. |
|
i was thinking about it from the point of view of simplifying the interface, since the cost is small (a few hundred) and would be neglible (probably exactly 110) if we switch the sysvar syscall backends to all go through if we think the cost of |
a2e1d6d to
fd859ba
Compare
|
per our call we are going to move forward with this, but i put it in draft because i realized i can do something better to tighten the interface now, will put it back in for review after |
da2907f to
4f8fca7
Compare
| // this unusual function exists to enforce stricter constraints on the new interface without affecting the old | ||
| // we call this where the old interface expected sysvar/config followed by an authority it did not assert must be present | ||
| // thus if we do *not* see sysvar/config, we *can* assert authority, knowing that there being *no* account is always an error | ||
| // doing it this way is non-breaking, and we can consider breaking the old interface after people switch over | ||
| // we return () to prevent refactors where the caller uses the result, because it might not be the desired account | ||
| fn consume_next_account<'a, 'b: 'a, I: Iterator<Item = &'a AccountInfo<'b>>>( | ||
| iter: &mut I, | ||
| ) -> Result<(), ProgramError> { | ||
| let account_info = iter.next().ok_or(ProgramError::NotEnoughAccountKeys)?; | ||
| if is_stake_program_sysvar_or_config(*account_info.key) || account_info.is_signer { | ||
| Ok(()) | ||
| } else { | ||
| Err(ProgramError::NotEnoughAccountKeys) | ||
| } | ||
| } |
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.
not sure if this comment is clear enough because its a bit tricky and makes sense in my head. the idea is if we have an instruction like:
- vote
- stake
- clock
- authority (native didnt enforce this exists at all, and bpf emulated native exactly)
we can safely do:
- vote
- stake
- authority (must be present, must be signer)
because if clock isnt there we know we are using our new instruction builders, and clock being there in the old instructions means we never have a valid case where there is no account. so we can gracefully add the constraint because there is always some account there
maybe a better way of putting it is, by doing this all at once we support an old 3 or 4 account case, and a new 3 account case, not creating more debt for ourselves by ever allowing a 2 account case in between
feel free to tell me im overthinking this but what im imagining is a process maybe over the next couple years like:
- check
SplitandSetLockuptransactions on mainnet to see if people actually omit the last account, if not enforce it as a breaking update - someday if/when everything is using the sysvar-less instruction builders, delete the sysvar-skipping code and enforce the accounts as defined by the new interface
- get rid of the signers hashmaps and move to a fully positional program interface like a typical bpf program
its possible there are nonupgradeable cpi callers of stake, in which case we would never get to do this, but i think at least consume_next_account() moves us in the right direction
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 makes sense to me overall! I know we discussed not wanting to break this during the BPF transition, so now seems like a good time to break.
Would you mind adding the first part of your comment here as a code comment? Specifically:
the idea is if we have an instruction like:
1. vote
2. stake
3. clock
4. authority (native didnt enforce this exists at all, and bpf emulated native exactly)
we can safely do:
1. vote
2. stake
3. authority (must be present, must be signer)
because if clock isnt there we know we are using our new instruction builders, and clock being there in the old instructions means we never have a valid case where there is no account. so we can gracefully add the constraint because there is always some account there
…_next_normal_account
2945f85 to
f9be148
Compare
joncinque
left a comment
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.
Great work overall! This is probably the best way to make this kind of sensitive change. Just some minor comments
| // this unusual function exists to enforce stricter constraints on the new interface without affecting the old | ||
| // we call this where the old interface expected sysvar/config followed by an authority it did not assert must be present | ||
| // thus if we do *not* see sysvar/config, we *can* assert authority, knowing that there being *no* account is always an error | ||
| // doing it this way is non-breaking, and we can consider breaking the old interface after people switch over | ||
| // we return () to prevent refactors where the caller uses the result, because it might not be the desired account | ||
| fn consume_next_account<'a, 'b: 'a, I: Iterator<Item = &'a AccountInfo<'b>>>( | ||
| iter: &mut I, | ||
| ) -> Result<(), ProgramError> { | ||
| let account_info = iter.next().ok_or(ProgramError::NotEnoughAccountKeys)?; | ||
| if is_stake_program_sysvar_or_config(*account_info.key) || account_info.is_signer { | ||
| Ok(()) | ||
| } else { | ||
| Err(ProgramError::NotEnoughAccountKeys) | ||
| } | ||
| } |
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 makes sense to me overall! I know we discussed not wanting to break this during the BPF transition, so now seems like a good time to break.
Would you mind adding the first part of your comment here as a code comment? Specifically:
the idea is if we have an instruction like:
1. vote
2. stake
3. clock
4. authority (native didnt enforce this exists at all, and bpf emulated native exactly)
we can safely do:
1. vote
2. stake
3. authority (must be present, must be signer)
because if clock isnt there we know we are using our new instruction builders, and clock being there in the old instructions means we never have a valid case where there is no account. so we can gracefully add the constraint because there is always some account there
| // required accounts | ||
| let stake_account_info = next_account_to_use(account_info_iter)?; |
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'm not 100% sure about this, but how about adding a comment for additional accounts that might be present? I'm worried about cleanup in the future that accidentally removes these.
On the other hand, is the idea to do another breaking change in the future where we replace next_account_to_use with next_account_info everywhere? And in the meantime, completely prohibit next_account_info? If so, then I'm fine with the code as is
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, i removed all uses of next_account_info(). ideally after we release the instruction builder update, everything migrates, and we have one program interface where we go to next_account_info() and sysvars are an error. less ideally if there are important frozen programs or lagging third parties, we leave it like this
| let _stake_or_withdraw_authority_info = next_account_info(account_info_iter)?; | ||
| // required accounts | ||
| let stake_account_info = next_account_to_use(account_info_iter)?; | ||
| let _stake_or_withdraw_authority_info = next_account_to_use(account_info_iter)?; |
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.
Shouldn't this be consume_next_account to do the signer 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.
it cant be consume_next_account(), we need next_account_to_use() to call next() twice (skip clock and grab authority)
whether to enforce this is a signer is an interesting question though... in this pr, i only used consume_next_account() for accounts we never checked must be there at all. in native, this corresponds to the asserts it did like "grab account, grab sysvar, assert length of accounts is 2, there must be an authority somewhere but stop here and rely on the signers hashset." in bpf, it means commented-out accounts like in Merge. the pattern here in Authorize, in native, continues "grab another account, assert length of accounts is 3, dont check if signer rely on hashet". we cant use either new utility function as-is to assert authority here is a signer because we would have to know if the previous next_account_to_use() skipped a clock or not
however the comments make me think in my attempt to simplify the interface for callers i have actually made it too confusing for maintainers. i think i have to rethink my approach to 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.
Ah sorry you're right, this might just be me being dense. I wrongly thought consume_next_account also did a loop.
Up to you how you want to do it!
| // thus if we do *not* see sysvar/config, we *can* assert authority, knowing that there being *no* account is always an error | ||
| // doing it this way is non-breaking, and we can consider breaking the old interface after people switch over | ||
| // we return () to prevent refactors where the caller uses the result, because it might not be the desired account | ||
| fn consume_next_account<'a, 'b: 'a, I: Iterator<Item = &'a AccountInfo<'b>>>( |
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: maybe we can name this a bit more clearly. How about consume_sysvar_account_or_signer?
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.
hmm i feel like that would imply it doesnt consume the account in other cases. next() is unconditional and then the behavior difference is ok/err
|
|
||
| // other accounts | ||
| // let _stake_authority_info = next_account_info(account_info_iter); | ||
| let _stake_authority_info = consume_next_account(account_info_iter)?; |
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: consume_next_account doesn't return an account info, so we should either change it to return the account info or change callers to not assign a named value
| // NOTE we cannot consume this account without a breaking change | ||
| // its presence was never enforced and `Split` never accepted sysvars as args | ||
| // we may decide to enforce this as a breaking change if the pattern is not used on mainnet | ||
| // let _stake_authority_info = next_account_to_use(account_info_iter)?; |
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 could go halfway and say "if this is account is present, it must be a signer". What do you think?
| // NOTE we cannot consume this account without a breaking change | ||
| // its presence was never enforced and `SetLockup` never accepted sysvars as args | ||
| // we may decide to enforce this as a breaking change if the pattern is not used on mainnet | ||
| // let _old_withdraw_or_lockup_authority_info = next_account_to_use(account_info_iter)?; |
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 with this one, how about we enforce "if it's present, it must be a signer"
|
doing this in #126 instead |
first half of #53. this updates the program to gracefully handle the old instruction builders (sysvars are passed in) and the expected new ones (they will not be). it does this by skipping them whenever they appear in the accounts list
we also take this opportunity to enforce that authorities must be present, and must be signers, for instructions that previously had sysvars before the technically optional authority
also updated some comments to reflect the fact that the program is now live on mainnet