-
Notifications
You must be signed in to change notification settings - Fork 308
wormhole_attester: per-symbol PDAs for attestation state #567
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
wormhole_attester: per-symbol PDAs for attestation state #567
Conversation
This change bumps price batch format to v3.1 with a new backwards compatible field - prev_attestation_time. This is the last time we've successfully attested the price. If no prior record exists, the current time is used (the same as attestation_time). The new field is backed by a new PDA for the attester contract, called 'attestation state'. In this PDA, we store a Pubkey -> Metadata hashmap for every price. Currently, the metadata stores just the latest successful attestation timestamp for use with the new field.
…ttester-prev-timestamp-field
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎ 2 Ignored Deployments
|
Using Option<> for this makes fallback to latest value more convenient
| pub pyth_state: AttestationStatePDA<'b>, | ||
| pub pyth_price: Info<'b>, | ||
|
|
||
| pub pyth_product2: Option<Info<'b>>, | ||
| pub pyth_price2: Option<Info<'b>>, | ||
| pub pyth_state2: Option<AttestationStatePDA<'b>>, | ||
| pub pyth_price2: Option<Info<'b>>, | ||
|
|
||
| pub pyth_product3: Option<Info<'b>>, | ||
| pub pyth_price3: Option<Info<'b>>, | ||
| pub pyth_state3: Option<AttestationStatePDA<'b>>, | ||
| pub pyth_price3: Option<Info<'b>>, | ||
|
|
||
| pub pyth_product4: Option<Info<'b>>, | ||
| pub pyth_price4: Option<Info<'b>>, | ||
| pub pyth_state4: Option<AttestationStatePDA<'b>>, | ||
| pub pyth_price4: Option<Info<'b>>, | ||
|
|
||
| pub pyth_product5: Option<Info<'b>>, | ||
| pub pyth_price5: Option<Info<'b>>, | ||
| pub pyth_state5: Option<AttestationStatePDA<'b>>, | ||
| pub pyth_price5: Option<Info<'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.
I guess you need to make the states mutable here
ali-behjati
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.
lgtm, i'll approve once the CI works. It seems like a mutability issue that I think is related to considering the state accounts mutable (on the contract)
| state.0.info(), | ||
| accs.payer.key, | ||
| solitaire::CreationLamports::Exempt, | ||
| state_serialized.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.
Instead of using state_serialized.len(), you can create a constant AttestationState::LEN which is equal to the maximum size the account can have. Then you can deleting all the resizing code.
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.
Or turn last_attested_trading_publish_time into a Timestamp instead of an Option (the comment below) and use the AccountSize solitaire trait.
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 I prefer spending the extra gas in exchange for not maintaining a constant for this. I expect us to want more metadata in this state at some point, if we don't drop the attester in the future. I think without the realloc logic it can live in the uninitialized if branch, so this is only done once in a PDA's lifetime ( that's the case in the change that removes the unneeded realloc()). Fundamentally, I think Solitaire could benefit from a flag that triggers this call for uninitialized data accounts. On persist() it serializes the struct anyway.
| pub struct AttestationState { | ||
| /// The last trading publish_time this attester saw | ||
| pub last_attested_trading_publish_time: UnixTimestamp, | ||
| pub last_attested_trading_publish_time: Option<UnixTimestamp>, |
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 think you need this to be an option.
Because the option logic is already captured in whether the account is initialized or not.
i.e. if the account doesn't exist it's a none
if it exists it's a some
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.
That makes sense. Changed to just look at is_initialized
| Data<'b, AttestationStateMap, { AccountState::MaybeInitialized }>, | ||
| "p2w-attestation-state-v1", | ||
| >; | ||
| impl<'a, 'b: 'a> Peel<'a, 'b> for AttestationStatePDA<'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.
Surprised you have to derive peel and persist manually. Why can't solitaire handle 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.
tl;dr that's because automating this in a derive macro is surprisingly difficult and Solitaire cannot do this currently
Going on a bit of a framework design tangent, it's actually a very interesting topic. I think a solid dedicated derive(Peel) could make this go away. The thing with Solana account deserialization is that aimost any attempt at a framework will focus on adding behavior modifiers on some sort of user accounts struct, one way or another. Anchor chose to go with macro attributes as the first class citizen, where you just specify requirements in the spirit of ETH modifiers, onto struct members as part of the derive macro. Solitaire went a different direction to do as little simple codegen as necessary. The idea being to contain as much of the validation logic in regular Rust code instead of the derive macro. So the way to enforce specific rules is abstracted away from the derive macro (Notice how it says FromAccounts and not Peel). The macro only knows that all members should implement Peel and it just calls that on each account in the incoming slice on-chain. Thus most automated validation comes from stacking types like Mut<Derive<Data<MyDataStruct, Initialized>>>, because they implement Peel and try to cover most simple cases. There are several unanticipated problems with our approach.First,t Solana's Rust safety choices make it very hard to come up with easy to use generics (we have to use lifetimes like 'a, 'b and so on). Also, our struct-based strategy that makes very little info available to the derive macro means that relationships between account struct members are almost impossible to express statically in struct definition
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.
AHHHH ok I think I get what's the problem
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.
You can get away with this by making :
pub type AttestationStatePDA<'b> =Data<'b, AttestationState, { AccountState::MaybeInitialized }>
instead of :
pub struct AttestationStatePDA<'b>(
pub Data<'b, AttestationState, { AccountState::MaybeInitialized }>,
);
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 this is just a strict simplification of the code
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.
That's exactly what I tried to do before I needed to add the Seeded impl. However, It is not possible to implement foreign traits for foreign types in Rust :( In this case, the typedef is considered equivalent to the foreign type we assign to it, so the compiler yells, because the trait is also foreign.
guibescos
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.
Some comments
ali-behjati
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.
🔥
jayantk
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.
i'm happy if @ali-bahjati and @guibescos are
| // Serialize the state to calculate rent/account size adjustments | ||
| let state_serialized = state.0 .0 .1.try_to_vec()?; | ||
|
|
||
| if state.0 .0.is_initialized() { |
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.
extract a helper function for all of this account allocation logic
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 this realloc logic needs to go away. This account is fixed sized. It should never get realloc and this code is too complex.
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.
You're right, this made sense only for the large PDA. Thanks.
This change replaces the big attestation state PDA with individual per-symbol ones. This ensures more predictable tx costs and is generally cheaper. The change is achieved by reusing the product accounts on the attest() endpoint and putting the state PDAs in their place.