[solana] Post update atomic (#1210)

* Post update atomic

* Checkpoint cli

* Fix bug

* Atomic

* Cleanup

* Cleanup

* Cleanup

* Cleanup

* Comment

* Add wormhole errors

* refactor trim signatures
This commit is contained in:
guibescos 2024-01-08 14:27:03 +00:00 committed by GitHub
parent 2d5e030149
commit 3458dd5b7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 368 additions and 61 deletions

View File

@ -3027,6 +3027,7 @@ dependencies = [
"solana-sdk",
"tokio",
"wormhole-core-bridge-solana",
"wormhole-raw-vaas",
"wormhole-sdk",
]

View File

@ -40,6 +40,17 @@ pub enum Action {
#[clap(short = 'p', long, help = "Payload from Hermes")]
payload: String,
},
#[clap(about = "Post a price update from Hermes to Solana in one transaction")]
PostPriceUpdateAtomic {
#[clap(short = 'p', long, help = "Payload from Hermes")]
payload: String,
#[clap(
short = 'n',
default_value = "5",
help = "Number of signatures to verify. If n >= 5 this will fail because of the transaction size limit."
)]
n_signatures: usize,
},
#[clap(
about = "Initialize a wormhole receiver contract by sequentially replaying the guardian set updates"
)]

View File

@ -1,7 +1,8 @@
#![deny(warnings)]
pub mod cli;
use {
anchor_client::anchor_lang::{
InstructionData,
@ -14,7 +15,10 @@ use {
Action,
Cli,
},
pyth_solana_receiver::state::config::DataSource,
pyth_solana_receiver::{
state::config::DataSource,
PostUpdatesAtomicParams,
},
pythnet_sdk::wire::v1::{
AccumulatorUpdateData,
MerklePriceUpdate,
@ -102,6 +106,25 @@ fn main() -> Result<()> {
&merkle_price_updates[0],
)?;
}
Action::PostPriceUpdateAtomic {
payload,
n_signatures,
} => {
let rpc_client = RpcClient::new(url);
let payer =
read_keypair_file(&*shellexpand::tilde(&keypair)).expect("Keypair not found");
let (vaa, merkle_price_updates) = deserialize_accumulator_update_data(&payload)?;
process_post_price_update_atomic(
&rpc_client,
&vaa,
n_signatures,
&wormhole,
&payer,
&merkle_price_updates[0],
)?;
}
Action::InitializeWormholeReceiver {} => {
let rpc_client = RpcClient::new(url);
@ -140,6 +163,7 @@ fn main() -> Result<()> {
false,
)?;
}
Action::InitializePythReceiver {
fee,
emitter,
@ -221,6 +245,55 @@ pub fn process_upgrade_guardian_set(
Ok(())
}
pub fn process_post_price_update_atomic(
rpc_client: &RpcClient,
vaa: &[u8],
n_signatures: usize,
wormhole: &Pubkey,
payer: &Keypair,
merkle_price_update: &MerklePriceUpdate,
) -> Result<Pubkey> {
let price_update_keypair = Keypair::new();
let (mut header, body): (Header, Body<&RawMessage>) = serde_wormhole::from_slice(vaa).unwrap();
trim_signatures(&mut header, n_signatures);
let request_compute_units_instruction: Instruction =
ComputeBudgetInstruction::set_compute_unit_limit(400_000);
let post_update_accounts = pyth_solana_receiver::accounts::PostUpdatesAtomic::populate(
payer.pubkey(),
price_update_keypair.pubkey(),
*wormhole,
header.guardian_set_index,
)
.to_account_metas(None);
let post_update_instruction = Instruction {
program_id: pyth_solana_receiver::id(),
accounts: post_update_accounts,
data: pyth_solana_receiver::instruction::PostUpdatesAtomic {
params: PostUpdatesAtomicParams {
merkle_price_update: merkle_price_update.clone(),
vaa: serde_wormhole::to_vec(&(header, body)).unwrap(),
},
}
.data(),
};
process_transaction(
rpc_client,
vec![request_compute_units_instruction, post_update_instruction],
&vec![payer, &price_update_keypair],
)?;
Ok(price_update_keypair.pubkey())
}
fn trim_signatures(header: &mut Header, n_signatures: usize) {
header.signatures = header.signatures[..(n_signatures)].to_vec();
}
fn deserialize_guardian_set(buf: &mut &[u8], legacy_guardian_set: bool) -> Result<GuardianSet> {
if !legacy_guardian_set {
// Skip anchor discriminator

View File

@ -21,6 +21,7 @@ pythnet-sdk = { path = "../../../../pythnet/pythnet_sdk", version = "2.0.0" }
solana-program = "1.16.20"
byteorder = "1.4.3"
wormhole-core-bridge-solana = {git = "https://github.com/guibescos/wormhole", branch = "variable-sigs"}
wormhole-raw-vaas = {version = "0.0.1-alpha.1", features = ["ruint", "on-chain"], default-features = false }
wormhole-sdk = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" }
serde_wormhole = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1"}

View File

@ -28,4 +28,15 @@ pub enum ReceiverError {
NonexistentGovernanceAuthorityTransferRequest,
#[msg("Funds are insufficient to pay the receiving fee")]
InsufficientFunds,
// Wormhole errors
#[msg("Invalid VAA version")]
InvalidVaaVersion,
#[msg("Guardian set version in the VAA doesn't match the guardian set passed")]
GuardianSetMismatch,
#[msg("Guardian index exceeds the number of guardians in the set")]
InvalidGuardianIndex,
#[msg("A VAA signature is invalid")]
InvalidSignature,
#[msg("The recovered guardian public key doesn't match the guardian set")]
InvalidGuardianKeyRecovery,
}

View File

@ -18,7 +18,12 @@ use {
},
},
serde_wormhole::RawMessage,
solana_program::system_instruction,
solana_program::{
keccak,
program_memory::sol_memcpy,
secp256k1_recover::secp256k1_recover,
system_instruction,
},
state::{
config::{
Config,
@ -26,7 +31,17 @@ use {
},
price_update::PriceUpdateV1,
},
wormhole_core_bridge_solana::state::EncodedVaa,
wormhole_core_bridge_solana::{
sdk::legacy::AccountVariant,
state::{
EncodedVaa,
GuardianSet,
},
},
wormhole_raw_vaas::{
GuardianSetSig,
Vaa,
},
wormhole_sdk::vaa::{
Body,
Header,
@ -36,7 +51,6 @@ use {
pub mod error;
pub mod state;
declare_id!("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ");
#[program]
@ -90,80 +104,115 @@ pub mod pyth_solana_receiver {
Ok(())
}
/// Post a price update using an encoded_vaa account and a MerklePriceUpdate calldata.
/// This should be called after the client has already verified the Vaa via the Wormhole contract.
/// Check out target_chains/solana/cli/src/main.rs for an example of how to do this.
#[allow(unused_variables)]
pub fn post_updates(ctx: Context<PostUpdates>, price_update: MerklePriceUpdate) -> Result<()> {
/// Post a price update using a VAA and a MerklePriceUpdate.
/// This function allows you to post a price update in a single transaction.
/// Compared to post_updates, it is less secure since you won't be able to verify all guardian signatures if you use this function because of transaction size limitations.
/// Typically, you can fit 5 guardian signatures in a transaction that uses this.
pub fn post_updates_atomic(
ctx: Context<PostUpdatesAtomic>,
params: PostUpdatesAtomicParams,
) -> Result<()> {
// This section is borrowed from https://github.com/wormhole-foundation/wormhole/blob/wen/solana-rewrite/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/verify_encoded_vaa_v1.rs#L59
let vaa = Vaa::parse(&params.vaa).map_err(|_| ReceiverError::DeserializeVaaFailed)?;
// Must be V1.
require_eq!(vaa.version(), 1, ReceiverError::InvalidVaaVersion);
// Make sure the encoded guardian set index agrees with the guardian set account's index.
let guardian_set = ctx.accounts.guardian_set.inner();
require_eq!(
vaa.guardian_set_index(),
guardian_set.index,
ReceiverError::GuardianSetMismatch
);
// Do we have enough signatures for quorum?
let guardian_keys = &guardian_set.keys;
// Generate the same message hash (using keccak) that the Guardians used to generate their
// signatures. This message hash will be hashed again to produce the digest for
// `secp256k1_recover`.
let digest = keccak::hash(keccak::hash(vaa.body().as_ref()).as_ref());
let mut last_guardian_index = None;
for sig in vaa.signatures() {
// We do not allow for non-increasing guardian signature indices.
let index = usize::from(sig.guardian_index());
if let Some(last_index) = last_guardian_index {
require!(index > last_index, ReceiverError::InvalidGuardianIndex);
}
// Does this guardian index exist in this guardian set?
let guardian_pubkey = guardian_keys
.get(index)
.ok_or_else(|| error!(ReceiverError::InvalidGuardianIndex))?;
// Now verify that the signature agrees with the expected Guardian's pubkey.
verify_guardian_signature(&sig, guardian_pubkey, digest.as_ref())?;
last_guardian_index = Some(index);
}
// End borrowed section
let config = &ctx.accounts.config;
let payer = &ctx.accounts.payer;
let encoded_vaa = &ctx.accounts.encoded_vaa;
let treasury = &ctx.accounts.treasury;
let price_update_account = &mut ctx.accounts.price_update_account;
if payer.lamports()
< Rent::get()?
.minimum_balance(0)
.saturating_add(config.single_update_fee_in_lamports)
{
return err!(ReceiverError::InsufficientFunds);
let vaa_components = VaaComponents {
verified_signatures: vaa.signature_count(),
emitter_address: vaa.body().emitter_address(),
emitter_chain: vaa.body().emitter_chain(),
};
let transfer_instruction = system_instruction::transfer(
payer.key,
treasury.key,
config.single_update_fee_in_lamports,
);
anchor_lang::solana_program::program::invoke(
&transfer_instruction,
&[
ctx.accounts.payer.to_account_info(),
ctx.accounts.treasury.to_account_info(),
],
post_price_update_from_vaa(
config,
payer,
treasury,
price_update_account,
&vaa_components,
vaa.payload().as_ref(),
&params.merkle_price_update,
)?;
let (header, body): (Header, Body<&RawMessage>) =
serde_wormhole::from_slice(&ctx.accounts.encoded_vaa.buf).unwrap();
let valid_data_source = config.valid_data_sources.iter().any(|x| {
*x == DataSource {
chain: body.emitter_chain.into(),
emitter: Pubkey::from(body.emitter_address.0),
}
});
if !valid_data_source {
return err!(ReceiverError::InvalidDataSource);
}
Ok(())
}
let wormhole_message = WormholeMessage::try_from_bytes(body.payload)
.map_err(|_| ReceiverError::InvalidWormholeMessage)?;
let root: MerkleRoot<Keccak160> = MerkleRoot::new(match wormhole_message.payload {
WormholePayload::Merkle(merkle_root) => merkle_root.root,
});
/// Post a price update using an encoded_vaa account and a MerklePriceUpdate calldata.
/// This should be called after the client has already verified the Vaa via the Wormhole contract.
/// Check out target_chains/solana/cli/src/main.rs for an example of how to do this.
pub fn post_updates(ctx: Context<PostUpdates>, price_update: MerklePriceUpdate) -> Result<()> {
let config = &ctx.accounts.config;
let payer: &Signer<'_> = &ctx.accounts.payer;
let encoded_vaa = &ctx.accounts.encoded_vaa;
let treasury: &AccountInfo<'_> = &ctx.accounts.treasury;
let price_update_account: &mut Account<'_, PriceUpdateV1> =
&mut ctx.accounts.price_update_account;
if !root.check(price_update.proof, price_update.message.as_ref()) {
return err!(ReceiverError::InvalidPriceUpdate);
}
let (_, body): (Header, Body<&RawMessage>) =
serde_wormhole::from_slice(&encoded_vaa.buf).unwrap();
let message = from_slice::<byteorder::BE, Message>(price_update.message.as_ref())
.map_err(|_| ReceiverError::DeserializeMessageFailed)?;
let vaa_components = VaaComponents {
verified_signatures: encoded_vaa.header.verified_signatures,
emitter_address: body.emitter_address.0,
emitter_chain: body.emitter_chain.into(),
};
match message {
Message::PriceFeedMessage(price_feed_message) => {
price_update_account.write_authority = payer.key();
price_update_account.verified_signatures = encoded_vaa.header.verified_signatures;
price_update_account.price_message = price_feed_message;
}
Message::TwapMessage(twap_message) => {
return err!(ReceiverError::UnsupportedMessageType);
}
}
post_price_update_from_vaa(
config,
payer,
treasury,
price_update_account,
&vaa_components,
body.payload,
&price_update,
)?;
Ok(())
}
}
pub const CONFIG_SEED: &str = "config";
pub const TREASURY_SEED: &str = "treasury";
@ -199,7 +248,6 @@ pub struct AuthorizeGovernanceAuthorityTransfer<'info> {
pub config: Account<'info, Config>,
}
#[derive(Accounts)]
pub struct PostUpdates<'info> {
#[account(mut)]
@ -217,6 +265,35 @@ pub struct PostUpdates<'info> {
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct PostUpdatesAtomic<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
seeds = [
GuardianSet::SEED_PREFIX,
guardian_set.inner().index.to_be_bytes().as_ref()
],
seeds::program = config.wormhole,
bump,
owner = config.wormhole)]
pub guardian_set: Account<'info, AccountVariant<GuardianSet>>,
#[account(seeds = [CONFIG_SEED.as_ref()], bump)]
pub config: Account<'info, Config>,
#[account(mut, seeds = [TREASURY_SEED.as_ref()], bump)]
/// CHECK: This is just a PDA controlled by the program. There is currently no way to withdraw funds from it.
pub treasury: AccountInfo<'info>,
#[account(init, payer = payer, space = PriceUpdateV1::LEN)]
pub price_update_account: Account<'info, PriceUpdateV1>,
pub system_program: Program<'info, System>,
}
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
pub struct PostUpdatesAtomicParams {
pub vaa: Vec<u8>,
pub merkle_price_update: MerklePriceUpdate,
}
impl crate::accounts::Initialize {
pub fn populate(payer: &Pubkey) -> Self {
let config = Pubkey::find_program_address(&[CONFIG_SEED.as_ref()], &crate::ID).0;
@ -228,6 +305,36 @@ impl crate::accounts::Initialize {
}
}
impl crate::accounts::PostUpdatesAtomic {
pub fn populate(
payer: Pubkey,
price_update_account: Pubkey,
wormhole_address: Pubkey,
guardian_set_index: u32,
) -> Self {
let config = Pubkey::find_program_address(&[CONFIG_SEED.as_ref()], &crate::ID).0;
let treasury = Pubkey::find_program_address(&[TREASURY_SEED.as_ref()], &crate::ID).0;
let guardian_set = Pubkey::find_program_address(
&[
GuardianSet::SEED_PREFIX,
guardian_set_index.to_be_bytes().as_ref(),
],
&wormhole_address,
)
.0;
crate::accounts::PostUpdatesAtomic {
payer,
guardian_set,
config,
treasury,
price_update_account,
system_program: system_program::ID,
}
}
}
impl crate::accounts::PostUpdates {
pub fn populate(payer: Pubkey, encoded_vaa: Pubkey, price_update_account: Pubkey) -> Self {
let config = Pubkey::find_program_address(&[CONFIG_SEED.as_ref()], &crate::ID).0;
@ -243,3 +350,106 @@ impl crate::accounts::PostUpdates {
}
}
}
struct VaaComponents {
verified_signatures: u8,
emitter_address: [u8; 32],
emitter_chain: u16,
}
fn post_price_update_from_vaa<'info>(
config: &Account<'info, Config>,
payer: &Signer<'info>,
treasury: &AccountInfo<'info>,
price_update_account: &mut Account<'_, PriceUpdateV1>,
vaa_components: &VaaComponents,
vaa_payload: &[u8],
price_update: &MerklePriceUpdate,
) -> Result<()> {
if payer.lamports()
< Rent::get()?
.minimum_balance(0)
.saturating_add(config.single_update_fee_in_lamports)
{
return err!(ReceiverError::InsufficientFunds);
};
let transfer_instruction = system_instruction::transfer(
payer.key,
treasury.key,
config.single_update_fee_in_lamports,
);
anchor_lang::solana_program::program::invoke(
&transfer_instruction,
&[payer.to_account_info(), treasury.to_account_info()],
)?;
let valid_data_source = config.valid_data_sources.iter().any(|x| {
*x == DataSource {
chain: vaa_components.emitter_chain,
emitter: Pubkey::from(vaa_components.emitter_address),
}
});
if !valid_data_source {
return err!(ReceiverError::InvalidDataSource);
}
let wormhole_message = WormholeMessage::try_from_bytes(vaa_payload)
.map_err(|_| ReceiverError::InvalidWormholeMessage)?;
let root: MerkleRoot<Keccak160> = MerkleRoot::new(match wormhole_message.payload {
WormholePayload::Merkle(merkle_root) => merkle_root.root,
});
if !root.check(price_update.proof.clone(), price_update.message.as_ref()) {
return err!(ReceiverError::InvalidPriceUpdate);
}
let message = from_slice::<byteorder::BE, Message>(price_update.message.as_ref())
.map_err(|_| ReceiverError::DeserializeMessageFailed)?;
match message {
Message::PriceFeedMessage(price_feed_message) => {
price_update_account.write_authority = payer.key();
price_update_account.verified_signatures = vaa_components.verified_signatures;
price_update_account.price_message = price_feed_message;
}
Message::TwapMessage(_) => {
return err!(ReceiverError::UnsupportedMessageType);
}
}
Ok(())
}
/**
* Borrowed from https://github.com/wormhole-foundation/wormhole/blob/wen/solana-rewrite/solana/programs/core-bridge/src/processor/parse_and_verify_vaa/verify_encoded_vaa_v1.rs#L121
*/
fn verify_guardian_signature(
sig: &GuardianSetSig,
guardian_pubkey: &[u8; 20],
digest: &[u8],
) -> Result<()> {
// Recover using `solana_program::secp256k1_recover`. Public key recovery costs 25k compute
// units. And hashing this public key to recover the Ethereum public key costs about 13k.
let recovered = {
// Recover EC public key (64 bytes).
let pubkey = secp256k1_recover(digest, sig.recovery_id(), &sig.rs())
.map_err(|_| ReceiverError::InvalidSignature)?;
// The Ethereum public key is the last 20 bytes of keccak hashed public key above.
let hashed = keccak::hash(&pubkey.to_bytes());
let mut eth_pubkey = [0; 20];
sol_memcpy(&mut eth_pubkey, &hashed.0[12..], 20);
eth_pubkey
};
// The recovered public key should agree with the Guardian's public key at this index.
require!(
recovered == *guardian_pubkey,
ReceiverError::InvalidGuardianKeyRecovery
);
// Done.
Ok(())
}