Skip to content

Conversation

swimricky
Copy link
Contributor

@swimricky swimricky commented Nov 24, 2023

Summary

  • implement postAccumulatorUpdateDataVaa to verify hermes response and then invoke postVaa using CPI
  • implement postUpdates that uses the postedVaa account and the MerklePriceUpdates and verifies the updates
    • this ix doesn't currently save any price info on-chain its just doing verification using the merkle root and price_updates

Notes

  • in order to fit the data for the 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 data
  • the dependencies still need to be cleaned up. There's quite a bit of overlap in functionality from the wormhole repo, the solana_sdk fork, and the wormhole-anchor-sdk.
  • updated the anchor version to 0.28.0 & solana version to 1.16.20. this was also part of the reason to add in some of the extra dependencies.

To Do(separate PRs):

  • initialize a config account that will have the configurations for the emitter address, emitter chain and owner of the post vaa account along with an authority that can write to it (aka the remote executor).
  • update pythnet_sdk to add BorshSerialize/Deserialize for MerklePriceUpdate and Keccak160 so that it can be passed in directly to post_updates
  • make PR to wormhole-anchor-sdk to include GuardianSet as well as the verify_signatures and post_vaa ix
  • remove wormhole-solana dependency

Copy link

vercel bot commented Nov 24, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

2 Ignored Deployments
Name Status Preview Comments Updated (UTC)
example-oracle-amm ⬜️ Ignored (Inspect) Visit Preview Nov 30, 2023 8:55pm
xc-admin-frontend ⬜️ Ignored (Inspect) Visit Preview Nov 30, 2023 8:55pm

@swimricky swimricky requested review from jayantk, ali-behjati, Reisen and guibescos and removed request for jayantk November 24, 2023 00:19
Copy link
Contributor

@jayantk jayantk left a 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

/// * `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(
Copy link
Contributor

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?

Copy link
Collaborator

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.

Copy link
Contributor Author

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

Copy link
Contributor

@jayantk jayantk Nov 24, 2023

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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);
Copy link
Contributor

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)

Copy link
Contributor Author

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

vaa.emitter_chain(),
emitter_chain,
ReceiverError::InvalidEmitterChain
);
Copy link
Contributor

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.

Copy link
Contributor

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,
Copy link
Contributor

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);
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor Author

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

Copy link
Contributor Author

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>,
Copy link
Contributor

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

Copy link
Contributor Author

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)?;
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated this thanks

Copy link
Contributor

@guibescos guibescos left a 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.

@swimricky
Copy link
Contributor Author

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

Copy link
Contributor

@jayantk jayantk left a 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.)

vaa.key(),
expected_vaa_pubkey,
ReceiverError::InvalidVaaAccountKey
);
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor

@guibescos guibescos Nov 29, 2023

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.

Copy link
Contributor Author

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:

  1. the pubkey of the signature_set matches the posted_vaa's signature_set pubkey
  2. 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

  1. 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
  2. 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

Copy link
Contributor

@guibescos guibescos Nov 29, 2023

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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

// 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);
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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);

Copy link
Contributor Author

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
@swimricky swimricky requested a review from jayantk November 29, 2023 19:44
#[msg("Received an invalid signature set")]
InvalidSignatureSet,
#[msg("The pubkey of the the posted vaa account is invalid")]
InvalidVaaAccountKey,
Copy link
Contributor

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

Copy link
Contributor

@guibescos guibescos left a 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")
};
}
Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor Author

@swimricky swimricky Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@swimricky swimricky merged commit 8202181 into main Nov 30, 2023
@swimricky swimricky deleted the solana-receiver-decodeVaa-accumulator branch November 30, 2023 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants