-
Notifications
You must be signed in to change notification settings - Fork 279
Solana receiver decode vaa accumulator #1145
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
The latest updates on your projects. Learn more about Vercel for Git ↗︎ 2 Ignored Deployments
|
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.
Thanks. I think there are some issues here though -- see inline comments
target_chains/solana/programs/pyth_solana_receiver/src/error.rs
Outdated
Show resolved
Hide resolved
/// * `data` - Bytes of the AccumulatorUpdateData response from hermes with the updates omitted | ||
/// (i.e. the `updates` field is an empty array). The updates are removed so that | ||
/// all the data needed for postVaa can fit in one txn. | ||
pub fn post_accumulator_update_vaa( |
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 do we have this instruction? It's a thin wrapper around posting the VAA, so why not have the client call the post vaa instruction themselves?
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 adding some extra types and structs would help to separate the boundary between the pythnet wireformat and the solana receiver wireformat.
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 reason this is a wrapper instead of having the client do postVaa
directly was so that we could do on-chain verification of the header in the accumulatorUpdateData
using the AccumulatorUpdateData::try_from_slice
the client/cli code is currently doing the exact same thing when it first parses the accumulatorUpdateData bytes before it invokes this ix but i thought it would be good to have this verification on-chain 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.
I think the header verification check isn't adding any value here. Someone could circumvent this check by calling post vaa themselves, and then calling the subsequent update method.
All of our security checks need to happen in the post_updates
method itself. In this particular case, I don't think the magic number / version checks are security critical so they could potentially be skipped for solana (given that there's a sort of weird flow needed to do the vaa posting).
} | ||
|
||
#[derive(Default, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)] | ||
pub struct GuardianSet { |
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 doesn't seem to be used anywhere in the program code. please delete.
I see from the CLI this seems to duplicate something that already exists in wormhole (?) If so let's just use the wormhole version.
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.
in the cli, i'm currently using a mix of the wormhole repo, the wormhole repo fork with the solana-sdk and the wormhole-anchor-sdk.
the solana-sdk has this but it's using an outdated version of borsh/solana which was why i re-implemented this here for now.
ideally i would like to make a PR to the wormhole-anchor-sdk to add this and some of the other functionality from the sdk-solana repo and remove the solana-sdk fork dependency completely
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 move this struct to the CLI where it's actually used then?
let vaa = &ctx.accounts.posted_vaa; // let posted_vaa_data = PostedVaaData::try_deserialize_unchecked(&mut &**vaa.try_borrow_data()?)?; | ||
let wh_message = WormholeMessage::try_from_bytes(vaa.payload.as_slice()) | ||
.map_err(|_| ReceiverError::InvalidWormholeMessage)?; | ||
msg!("constructed wh_message {:?}", wh_message); |
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 checks for the message emitter need to be added here (and removed from the instruction constraints)
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 elaborate on why it needs to be here vs. as an instruction constraint?
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 added the message emitter checks here and in the postVaa wrapper. although there's no downside to having it in both, i think it makes more sense being checked in the postAccumulatorUpdateVaa ix than 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.
I believe someone can post a vaa directly (by calling PostVAA themselves), then pass that VAA account to this function. If they do that, any security checks in this function are bypassed. (This is also why i think this function is kind of useless)
These checks should not be an instruction constraint because the data source emitter (chain id and address) is configurable via a governance message on all other chains. Using the instruction constraint is much less flexible than using a configuration parameter.
Also you need to check not only that the chain is correct, but also that the emitter address is correct.
target_chains/solana/programs/pyth_solana_receiver/tests/program_test/mod.rs
Show resolved
Hide resolved
vaa.emitter_chain(), | ||
emitter_chain, | ||
ReceiverError::InvalidEmitterChain | ||
); |
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 you also need to check that the emitter address is the pyth program on the provided chain.
I suggest referencing the EVM implementation and making sure that every security check there is included 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.
(this comment is tied to the theme above -- every PR should leave the code in a state that is OK from a security perspective)
pub fn post_updates( | ||
ctx: Context<PostUpdates>, | ||
vaa_hash: [u8; 32], // used for pda seeds | ||
emitter_chain: u16, |
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.
don't put a security critical parameter as an argument. this is just asking for us to forget about this TODO and leave a security vuln in the codebase. I would prefer you hardcoding a constant with the TODO above than this.
In general, I think we should strive to ensure that every PR leaves the code in a state that is OK from a security perspective. It may not be as configurable or flexible as we would like, or the functionality may not be there, but at least it's secure. That approach ensures that our own human fallibility doesn't cause security holes in our code.
let vaa = &ctx.accounts.posted_vaa; // let posted_vaa_data = PostedVaaData::try_deserialize_unchecked(&mut &**vaa.try_borrow_data()?)?; | ||
let wh_message = WormholeMessage::try_from_bytes(vaa.payload.as_slice()) | ||
.map_err(|_| ReceiverError::InvalidWormholeMessage)?; | ||
msg!("constructed wh_message {:?}", wh_message); |
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 believe someone can post a vaa directly (by calling PostVAA themselves), then pass that VAA account to this function. If they do that, any security checks in this function are bypassed. (This is also why i think this function is kind of useless)
These checks should not be an instruction constraint because the data source emitter (chain id and address) is configurable via a governance message on all other chains. Using the instruction constraint is much less flexible than using a configuration parameter.
Also you need to check not only that the chain is correct, but also that the emitter address is correct.
#[allow(unused_variables)] | ||
pub fn post_updates( | ||
ctx: Context<PostUpdates>, | ||
vaa_hash: [u8; 32], // used for pda 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.
I don't think you need vaa_hash as a parameters here. We are trusting the posted VAA based on it being owned by wormhole + the discriminator
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 mind removing this but technically any vaa that has been posted by the wormhole program would be owned by wormhole and have the right discriminator. The merkle root prove would fail however in that situation if the wrong vaa was 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.
i removed the vaa_hash
from the ix parameters but added the signature_set account into the PostUpdates
context since it also contains the hash & we can verify that it's the correct signature_set since the vaa
has a field for the signature set pubkey.
)] | ||
pub posted_vaa: Box<Account<'info, AnchorVaa>>, | ||
/// CHECK: program that called post_vaa | ||
pub post_vaa_program: AccountInfo<'info>, |
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 you don't need this 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 removed this and changed the key derivation to be in the ix instead. right now im hard-coding that the owner of the posted_vaa program is the wormhole program but based off this feedback from @jayantk #1145 (comment)
i think we want it to be configurable so i will include it in the PR that adds a config account that only the authority can write to
return err!(ReceiverError::InvalidPriceUpdate); | ||
} | ||
let msg = from_slice::<byteorder::BE, Message>(&message_vec) | ||
.map_err(|_| ReceiverError::InvalidAccumulatorMessage)?; |
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 a DeserializedMessageFailed
?
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 this thanks
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.
For governance I recommend you just create a config account that has the config (emitter_address, emitter_chain) and an authority that can update this fields.
We can use the remote executor as the authority later.
thanks for this, that was the eventual plan but i'm going to add the config/governance in a separate PR since this one already got a little bit out of hand size wise |
…gnatureset for postupdates
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 is much improved from the last round. I have some minor comments, but also one blocking comment, which is:
I am confused about the VAA verification part of this code. What is the right way to check that we are getting a valid (meaning signature verified) VAA account? We shouldn't be inventing something here -- we should be using whatever the prescribed approach from wormhole is.
(Also, meta-comment: the code should be documented enough to explain what wormhole guarantees and why this approach is secure. I shouldn't be guessing as to the security here.)
target_chains/solana/programs/pyth-solana-receiver/src/error.rs
Outdated
Show resolved
Hide resolved
vaa.key(), | ||
expected_vaa_pubkey, | ||
ReceiverError::InvalidVaaAccountKey | ||
); |
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.
What are the security requirements for verifying that the posted VAA is correctly signed? I don't understand why we need the signature_set here. How do other programs do 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.
this wormhole example seems like it's using the vaa_hash https://github.com/wormhole-foundation/wormhole-scaffolding/blob/main/solana/programs/01_hello_world/src/context.rs#L243
not sure if that's the current best practice or not. hard to tell from their docs.
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 a VAA can only be posted after checking the signatures.
I think the requirements are checking the program owner and the discriminator.
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.
checking the vaa_pubkey is an extra layer of security. we have 1 layer from the merkle root in the vaa.payload and proving against it, then this vaa_pubkey check is an additional layer as well.
the signature_set is passed in so we can check 2 things:
- the pubkey of the signature_set matches the posted_vaa's signature_set pubkey
- the signature_set contains the vaa_hash which we use to derive the expected vaa_pubkey.
i was originally passing in the vaa_hash like the wormhole example you posted but switched to this implementation because of this discussion #1145 (comment)
wormhole checks the vaa a a few times
- verify_signatures when it initializes the signature_set account using the secp256k1 ix data - https://github.com/wormhole-foundation/wormhole/blob/main/solana/bridge/program/src/api/verify_signature.rs#L164-L169
- post_vaa
a. checking the key of the vaa account - https://github.com/wormhole-foundation/wormhole/blob/main/solana/bridge/program/src/api/post_vaa.rs#L107-L108C13
b. hashing the relevant fields and checking against the signature_set.hash - https://github.com/wormhole-foundation/wormhole/blob/main/solana/bridge/program/src/api/post_vaa.rs#L214
i took a look at the wormhole-token-bridge program completeTransfer ix since it logically follows a similar flow (verifySignatures -> postVaa -> completeTransfer). it doesn't seem to have any checks related the actual hash/key of the posted vaa, just some of the metadata and fields related to the actual token transfer
https://github.com/wormhole-foundation/wormhole/blob/main/solana/modules/token_bridge/program/src/api/complete_transfer.rs#L103-L118
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 still don't get passing the signature set signature set.
A posted VAA is a VAA whose signatures have already been checked.
People should be able to pass whatever posted VAA to this program, if the proof doesn't match the VAA it will just fail.
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 signature set is just a different way to pass the vaa_hash
since it also contains the hash of the data. this is just an extra check b/c like you said, if the proof doesn't match the vaa then it'll fail.
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 understand why you need this extra check, let's just keep it simple
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 ill remove this account then and update the comments that we're assuming wormhole has done the necessary security checks when the verify_signatures
& post_vaa
ixs were called and the merkle root from the posted_vaa.payload
+ proving will fail if the vaa is wrong anyways
target_chains/solana/programs/pyth-solana-receiver/src/state.rs
Outdated
Show resolved
Hide resolved
// https://docs.rs/solana-program/1.6.4/solana_program/bpf_loader_upgradeable/index.html | ||
let (programdata_key, _) = | ||
Pubkey::find_program_address(&[&program_key.to_bytes()], &bpf_loader_upgradeable::id()); | ||
let mut program_test = ProgramTest::new("pyth_solana_receiver", ID, None); |
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 really think you shouldn't change this deployment code. We're doing it this way for a reason (to test upgrades) and you're just going to have to rewrite it again.
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 will look into reverting this to original state
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 reset it back to the original state but just a note that i tried adding back the original update ix and using that test case and it failed with a strange InvalidAccountData
. I tried debugging the source of this but wasn't able to figure it out. i'll have to look into it later. The test runs fine when i use ProgramTest::new("pyth_solana_receiver", ID, None);
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 is there a repo that tests the program upgrade flow? https://github.com/pyth-network/pyth-client/blob/main/program/rust/src/tests/pyth_simulator.rs also has this setup but it doesn't look like the actual program upgrade is tested there, the upgrade authority is checked for the upd_permissions
test
add documentation and todos, revert programsimulator to use upgradable bpf loader
#[msg("Received an invalid signature set")] | ||
InvalidSignatureSet, | ||
#[msg("The pubkey of the the posted vaa account is invalid")] | ||
InvalidVaaAccountKey, |
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.
Many errors here are no longer 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.
It's looking good, I'll let Jayant take one last look
|
||
PathBuf::from(target_dir).join("deploy/pyth_solana_receiver.so") | ||
}; | ||
} |
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 is not the right way to do this. you should use a build.rs script to ensure the bpf gets built when you run the normal build. pyth-client has a good example
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 this can wait for a later pr though. this pr is pretty big already
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 pulled this from the pyth-client - https://github.com/pyth-network/pyth-client/blob/main/program/rust/src/tests/pyth_simulator.rs#L73-L108
…amically against config
Summary
postAccumulatorUpdateDataVaa
to verify hermes response and then invokepostVaa
using CPIpostUpdates
that uses the postedVaa account and theMerklePriceUpdates
and verifies the updatesNotes
postAccumulatorUpdateDataVaa
, the client has to take the hermes response, parse it and construct a version of it without the price_updates then serialize and send the bytes as part of the instruction dataTo Do(separate PRs):
MerklePriceUpdate
andKeccak160
so that it can be passed in directly topost_updates
GuardianSet
as well as the verify_signatures and post_vaa ix