-
Notifications
You must be signed in to change notification settings - Fork 278
wormhole-attester: Add a previous attestation timestamp field #488
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: Add a previous attestation timestamp field #488
Conversation
@@ -240,6 +267,51 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So | |||
|
|||
trace!("Attestations successfully created"); | |||
|
|||
// Serialize the state to calculate rent/account size adjustments | |||
let serialized = accs.attestation_state.1.try_to_vec()?; |
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 would make one PDA per price feed to avoid the complexity of resizing the 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.
Also potentially might congest the network because write access from many transactions to one 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.
about congestion, i had the same doubt, but Wormhole Sequence Account is already a bottleneck. So it won't be different.
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.
Ok then it's just about the extra complexity of having a variable size 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.
I think this is fine. We're probably not going to exceed the size of the one account, and PDAs have their own issues.
Honestly either way would probably work, but I don't think there's a strong reason to rewrite this to use PDAs.
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.
My main motivation for using a single account is a nasty edge case on Solana. In general, you're allowed to transfer SOL to any non-executable account, PDA or otherwise. For uninitialized accounts, such transfer instantiates the account and assigns ownership to the system program. When a program encounters such auto-initialized account as its own PDA, the program cannot change the contents, because it does not own it. This potentially creates a DoS condition for any known uninitialized PDA. This is completely permissionless and can happen on accident, which means that even Pythnet is not 100% immune. In fact, an accident is how I learned, some time ago while debugging a program I thought I was making a rent mistake so I tried pre-loading an account with SOL, locking the program out of it.
The attack surface in this situation is dependent on how often new PDAs need to be created and how easy (or not) it is to use a different PDA for the desired transaction. If we resort to a per-symbol PDA scheme, each new oracle symbol can potentially be vulnerable. What is even worse, if one symbol is denied its PDA, the only possible fix is to change the seeds inside program logic, losing all on-chain state for all the other accounts.
The proposed single-PDA solution collapses the attack surface into just a single race condition in the whole lifetime of this logic, along with relatively rare upgrades of the attestation state (we version this PDA's format in its seeds). Any lockout condition can be worked around by just altering its seeds.
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.
As for storage space, the account size on Solana is limited, but the current 10 MiB limit seems sufficient. E.g. for 10k symbols we could give a bit more than 1KiB space per symbol. IMO this is more symbols than we'll have in forseeable future, and more space than we'll need in forseeable future
@@ -60,9 +65,10 @@ pub const P2W_MAX_BATCH_SIZE: u16 = 5; | |||
#[derive(FromAccounts)] | |||
pub struct Attest<'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.
The Attest interface has changed. How do plan on achieving 0 downtime for attester cranks?
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.
yeah... what's the upgrade process here?
can you make the attestationstatemap optional so that if it's missing, it just sets the time intervals to 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.
One possible way to handle it smoothly is making the account optional but idk whether that's possible or not.
The other way is that we run a new attester with the new config that does not crash on failure and keep another one that works with the previous one. The new one should not crash on k8s as there is an exponential backoff on restarting the crashed pod.
Another approach is that attester can handle both types at the same time but i'm not sure that be easy to implement.
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 was thinking precisely about what Ali said. We run a new attester in parallel with the old one, before deploying new contract version. This new attester will keep crashing intentionally. Then, we perform the program upgrade. This should cause the old attester to start crashing, and the new one to pick up the ball immediately.
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.
Everything else should be relatively happy to utilize the new format. the wire format is extended in a backwards compatible way. Any parser that obeys the format should understand that there's trailing bytes it does not need to care for.
@@ -44,7 +44,7 @@ impl<'a> Seeded<&P2WMessageDrvData> for P2WMessage<'a> { | |||
// See the note at 2022-09-05 above. | |||
// Change the version in the literal whenever you change the | |||
// price attestation data. | |||
"p2w-message-v1".as_bytes().to_vec(), |
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.
Are you sure you need to update this? Doesn't having batch_size as a seed already solve 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.
The format had changed and payload size had changed. The batch size refers to the number of symbols being attested, not the total payload size in bytes.
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.
Hey Stan, since this will create new accounts, can you make sure our accounts will remain topped up by a lot of pgas when we do the deployment on Pythnet?
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 disagree with not using PDAs for each price feed AttestationState
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 need to be careful about exactly what timestamp we use here -- see inline comment. That shouldn't be too hard to fix, but i do want to make sure it is fixed before approving.
Aside from that, looks good to me.
#[serde(serialize_with = "use_to_string")] | ||
pub prev_conf: u64, | ||
pub prev_conf: u64, | ||
pub prev_attestation_time: 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 think we need to use a timestamp derived from the publish time not the attestation time for consistency. All the downstream SDKs use the publish time as the timestamp of the price update.
This will probably get confusing since there's already a field called prev_publish_time
. However, I think this field should be something like previously_attested_publish_time
which is set to either publish_time
or prev_publish_time
depending on whether the current status is trading or not (?).
I think that ensures that there's at most one update per time interval, and that each interval ends with a usable price. (If the feed goes offline, I think you get a bunch of price updates where the current update timestamp is equal to the previously_attested_publish_time
. When the feed comes back online, you get an update that spans the entire interval with the next available price.)
Please do check that logic and validate that it has the right properties. It's a little tricky.
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 like to suggest removing prev_* fields entirely and handling their logic directly here. We can do it in a way that it's not breaking (just pass 0 for those fields and replace one field with the field that we want)
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 understood some more context and I used publish_time
as proposed
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.
The name for the timestamp stored on chain is "last_attested_publish_time" to distinguish from existing PriceAttestation
fields.
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 don't change the prev_ fields right now. It's too much to change at once.
@@ -60,9 +65,10 @@ pub const P2W_MAX_BATCH_SIZE: u16 = 5; | |||
#[derive(FromAccounts)] | |||
pub struct Attest<'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.
yeah... what's the upgrade process here?
can you make the attestationstatemap optional so that if it's missing, it just sets the time intervals to 0?
if accs.attestation_state.is_initialized() { | ||
accs.attestation_state | ||
.info() | ||
.realloc(serialized.len(), false)?; |
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 there a performance consequence or something of doing this every time? Maybe check if you have enough space before reallocating?
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.
No as far as I can tell. Solana v1.10.31 AccountInfo implementation:
pub fn realloc(&self, new_len: usize, zero_init: bool) -> Result<(), ProgramError> {
let mut data = self.try_borrow_mut_data()?;
let old_len = data.len();
// Return early if length hasn't changed
if new_len == old_len {
return Ok(());
}
// Return early if the length increase from the original serialized data
// length is too large and would result in an out of bounds allocation.
let original_data_len = unsafe { self.original_data_len() };
if new_len.saturating_sub(original_data_len) > MAX_PERMITTED_DATA_INCREASE {
return Err(ProgramError::InvalidRealloc);
}
// realloc
unsafe {
let data_ptr = data.as_mut_ptr();
// First set new length in the serialized data
*(data_ptr.offset(-8) as *mut u64) = new_len as u64;
// Then recreate the local slice with the new length
*data = from_raw_parts_mut(data_ptr, new_len)
}
if zero_init {
let len_increase = new_len.saturating_sub(old_len);
if len_increase > 0 {
sol_memset(&mut data[old_len..], 0, len_increase);
}
}
Ok(())
}
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.
Re: only do increases in size. This is not that easy because borsh gets upset if it doesn't consume a whole buffer for deserialization.
/// Top-level state gathering all known AttestationState values, keyed by price address. | ||
#[derive(BorshSerialize, BorshDeserialize, Default)] | ||
pub struct AttestationStateMap { | ||
pub entries: BTreeMap<Pubkey, AttestationState>, |
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.
question for myself, will adding a BTreeMap
here result in a big code size increase?
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 necessarily, BTreeMap is a common data structure. It is likely already in use in our dependencies, thus no significant code size increase beyond the function calls to its methods. As for serialization payload size, it is probably using the borsh-specific key-value type, which depends on borsh traits for construction.
The latest updates on your projects. Learn more about Vercel for Git ↗︎ 1 Ignored Deployment
|
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.
600161f
to
c0d8cb0
Compare
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.