Просмотр исходного кода

Wormhole message reuse via post_message_unreliable (#261)

* pyth2wormhole: Implement reusable message PDAs

This changeset converts attest() to use a new PDA for
reusable/unreliable wormhole message account. Each PDA is tied to a
given attest() payer with an index that lets them rotate a number of
message accounts. Keeping the appropriate timing of the reuses is up
to the message owner, who should rotate a number of PDAs.

* pyth2wormhole-client: Add a message acc rotation impl

* p2w attest(): Integrate with bumped wormhole and fix call issues

* p2w-client: Format code, fix test_attest, refactor message index

* Dockerfile.client, Dockerfile.p2w-attest: Improve caching

The main improvement comes from running cargo-install from within a
workspace which lets us cache target/

* p2w-client: Make reusable messages configurable in yaml

* p2w on-chain: refactor to unreliable-only, adjust msg balance, nits

* p2w-client: P2WMessageIndex -> P2WMessageQueue, queue tests

* p2w-client: Add a hard limit to the message account queue
Stanisław Drozd 3 лет назад
Родитель
Сommit
6382f540db

+ 2 - 1
Dockerfile.client

@@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -yq libudev-dev ncat
 RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && apt-get install -y nodejs
 
 COPY solana /usr/src/solana 
-WORKDIR /usr/src/solana
+WORKDIR /usr/src/solana/pyth2wormhole
 
 RUN --mount=type=cache,target=/root/.cache \
     cargo install --version =2.0.12 --locked spl-token-cli
@@ -23,6 +23,7 @@ ENV BRIDGE_ADDRESS="Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"
 
 RUN --mount=type=cache,target=/root/.cache \
     --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \
+    --mount=type=cache,target=target,id=cargo_registry \
 	set -xe && \
     cargo install bridge_client --git https://github.com/certusone/wormhole --tag $WORMHOLE_TAG --locked --root /usr/local && \
     cargo install token_bridge_client --git https://github.com/certusone/wormhole --tag $WORMHOLE_TAG --locked --root /usr/local

+ 14 - 0
solana/pyth2wormhole/client/src/attestation_cfg.rs

@@ -15,6 +15,10 @@ use solana_program::pubkey::Pubkey;
 /// Pyth2wormhole config specific to attestation requests
 #[derive(Debug, Deserialize, Serialize, PartialEq)]
 pub struct AttestationConfig {
+    #[serde(default = "default_min_msg_reuse_interval_ms")]
+    pub min_msg_reuse_interval_ms: u64,
+    #[serde(default = "default_max_msg_accounts")]
+    pub max_msg_accounts: u64,
     pub symbol_groups: Vec<SymbolGroup>,
 }
 
@@ -26,6 +30,14 @@ pub struct SymbolGroup {
     pub symbols: Vec<P2WSymbol>,
 }
 
+pub const fn default_max_msg_accounts() -> u64 {
+    1_000_000
+}
+
+pub const fn default_min_msg_reuse_interval_ms() -> u64 {
+    10_000 // 10s
+}
+
 pub const fn default_min_interval_secs() -> u64 {
     60
 }
@@ -149,6 +161,8 @@ mod tests {
         };
 
         let cfg = AttestationConfig {
+            min_msg_reuse_interval_ms: 1000,
+            max_msg_accounts: 100_000,
             symbol_groups: vec![fastbois, slowbois],
         };
 

+ 3 - 1
solana/pyth2wormhole/client/src/cli.rs

@@ -116,7 +116,9 @@ pub enum Action {
         #[clap(long = "is-active")]
         is_active: Option<bool>,
     },
-    #[clap(about = "Migrate existing pyth2wormhole program settings to a newer format version. Client version must match the deployed contract.")]
+    #[clap(
+        about = "Migrate existing pyth2wormhole program settings to a newer format version. Client version must match the deployed contract."
+    )]
     Migrate {
         /// owner keypair path
         #[clap(

+ 51 - 45
solana/pyth2wormhole/client/src/lib.rs

@@ -1,5 +1,6 @@
 pub mod attestation_cfg;
 pub mod batch_state;
+pub mod message;
 pub mod util;
 
 use borsh::{
@@ -20,7 +21,13 @@ use solana_program::{
         rent,
     },
 };
-use solana_sdk::{transaction::Transaction, signer::{Signer, keypair::Keypair}};
+use solana_sdk::{
+    signer::{
+        keypair::Keypair,
+        Signer,
+    },
+    transaction::Transaction,
+};
 use solitaire::{
     processors::seeded::Seeded,
     AccountState,
@@ -41,7 +48,14 @@ use p2w_sdk::P2WEmitter;
 
 use pyth2wormhole::{
     attest::P2W_MAX_BATCH_SIZE,
-    config::{OldP2WConfigAccount, P2WConfigAccount},
+    config::{
+        OldP2WConfigAccount,
+        P2WConfigAccount,
+    },
+    message::{
+        P2WMessage,
+        P2WMessageDrvData,
+    },
     AttestData,
 };
 
@@ -58,6 +72,8 @@ pub use util::{
     RLMutexGuard,
 };
 
+pub use message::P2WMessageQueue;
+
 /// Future-friendly version of solitaire::ErrBox
 pub type ErrBoxSend = Box<dyn std::error::Error + Send + Sync>;
 
@@ -67,26 +83,22 @@ pub fn gen_init_tx(
     config: Pyth2WormholeConfig,
     latest_blockhash: Hash,
 ) -> Result<Transaction, ErrBox> {
-
     let payer_pubkey = payer.pubkey();
     let acc_metas = vec![
         // new_config
-        AccountMeta::new(P2WConfigAccount::<{AccountState::Uninitialized}>::key(None, &p2w_addr), false),
+        AccountMeta::new(
+            P2WConfigAccount::<{ AccountState::Uninitialized }>::key(None, &p2w_addr),
+            false,
+        ),
         // payer
         AccountMeta::new(payer.pubkey(), true),
         // system_program
         AccountMeta::new(system_program::id(), false),
-        ];
+    ];
 
     let ix_data = (pyth2wormhole::instruction::Instruction::Initialize, config);
 
-    let ix = Instruction::new_with_bytes(
-        p2w_addr,
-        ix_data
-            .try_to_vec()?
-            .as_slice(),
-        acc_metas,
-    );
+    let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
 
     let signers = vec![&payer];
 
@@ -110,25 +122,22 @@ pub fn gen_set_config_tx(
 
     let acc_metas = vec![
         // config
-        AccountMeta::new(P2WConfigAccount::<{AccountState::Initialized}>::key(None, &p2w_addr), false),
+        AccountMeta::new(
+            P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_addr),
+            false,
+        ),
         // current_owner
         AccountMeta::new(owner.pubkey(), true),
         // payer
         AccountMeta::new(payer.pubkey(), true),
-        ];
+    ];
 
     let ix_data = (
         pyth2wormhole::instruction::Instruction::SetConfig,
         new_config,
     );
 
-    let ix = Instruction::new_with_bytes(
-        p2w_addr,
-        ix_data
-            .try_to_vec()?
-            .as_slice(),
-        acc_metas,
-    );
+    let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
 
     let signers = vec![&owner, &payer];
     let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
@@ -146,12 +155,14 @@ pub fn gen_migrate_tx(
     owner: Keypair,
     latest_blockhash: Hash,
 ) -> Result<Transaction, ErrBox> {
-
     let payer_pubkey = payer.pubkey();
 
     let acc_metas = vec![
         // new_config
-        AccountMeta::new(P2WConfigAccount::<{AccountState::Uninitialized}>::key(None, &p2w_addr), false),
+        AccountMeta::new(
+            P2WConfigAccount::<{ AccountState::Uninitialized }>::key(None, &p2w_addr),
+            false,
+        ),
         // old_config
         AccountMeta::new(OldP2WConfigAccount::key(None, &p2w_addr), false),
         // owner
@@ -160,20 +171,11 @@ pub fn gen_migrate_tx(
         AccountMeta::new(payer.pubkey(), true),
         // system_program
         AccountMeta::new(system_program::id(), false),
-        ];
+    ];
 
-    let ix_data = (
-        pyth2wormhole::instruction::Instruction::Migrate,
-        (),
-    );
+    let ix_data = (pyth2wormhole::instruction::Instruction::Migrate, ());
 
-    let ix = Instruction::new_with_bytes(
-        p2w_addr,
-        ix_data
-            .try_to_vec()?
-            .as_slice(),
-        acc_metas,
-    );
+    let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
 
     let signers = vec![&owner, &payer];
 
@@ -209,8 +211,8 @@ pub fn gen_attest_tx(
     p2w_addr: Pubkey,
     p2w_config: &Pyth2WormholeConfig, // Must be fresh, not retrieved inside to keep side effects away
     payer: &Keypair,
+    wh_msg_id: u64,
     symbols: &[P2WSymbol],
-    wh_msg: &Keypair,
     latest_blockhash: Hash,
 ) -> Result<Transaction, ErrBoxSend> {
     let emitter_addr = P2WEmitter::key(None, &p2w_addr);
@@ -279,7 +281,16 @@ pub fn gen_attest_tx(
             false,
         ),
         // wh_message
-        AccountMeta::new(wh_msg.pubkey(), true),
+        AccountMeta::new(
+            P2WMessage::key(
+                &P2WMessageDrvData {
+                    id: wh_msg_id,
+                    message_owner: payer.pubkey(),
+                },
+                &p2w_addr,
+            ),
+            false,
+        ),
         // wh_emitter
         AccountMeta::new_readonly(emitter_addr, false),
         // wh_sequence
@@ -295,21 +306,16 @@ pub fn gen_attest_tx(
         pyth2wormhole::instruction::Instruction::Attest,
         AttestData {
             consistency_level: ConsistencyLevel::Confirmed,
+            message_account_id: wh_msg_id,
         },
     );
 
-    let ix = Instruction::new_with_bytes(
-        p2w_addr,
-        ix_data
-            .try_to_vec()?
-            .as_slice(),
-        acc_metas,
-    );
+    let ix = Instruction::new_with_bytes(p2w_addr, ix_data.try_to_vec()?.as_slice(), acc_metas);
 
     let tx_signed = Transaction::new_signed_with_payer::<Vec<&Keypair>>(
         &[ix],
         Some(&payer.pubkey()),
-        &vec![&payer, &wh_msg],
+        &vec![&payer],
         latest_blockhash,
     );
     Ok(tx_signed)

+ 18 - 8
solana/pyth2wormhole/client/src/main.rs

@@ -45,7 +45,10 @@ use solitaire::{
     ErrBox,
 };
 use tokio::{
-    sync::Semaphore,
+    sync::{
+        Mutex,
+        Semaphore,
+    },
     task::JoinHandle,
 };
 
@@ -136,16 +139,16 @@ async fn main() -> Result<(), ErrBox> {
                 get_config_account(&rpc_client, &p2w_addr).await?
             );
         }
-        Action::Migrate {
-            ref owner,
-        } => {
+        Action::Migrate { ref owner } => {
             let tx = gen_migrate_tx(
                 payer,
                 p2w_addr,
                 read_keypair_file(&*shellexpand::tilde(&owner))?,
                 latest_blockhash,
             )?;
-            rpc_client.send_and_confirm_transaction_with_spinner(&tx).await?;
+            rpc_client
+                .send_and_confirm_transaction_with_spinner(&tx)
+                .await?;
             println!(
                 "Applied conifg:\n{:?}",
                 get_config_account(&rpc_client, &p2w_addr).await?
@@ -252,6 +255,8 @@ async fn handle_attest(
         rpc_interval,
     ));
 
+    let message_q_mtx = Arc::new(Mutex::new(P2WMessageQueue::new(Duration::from_millis(attestation_cfg.min_msg_reuse_interval_ms), attestation_cfg.max_msg_accounts as usize)));
+
     // Create attestation scheduling routines; see attestation_sched_job() for details
     let mut attestation_sched_futs = batches.into_iter().map(|(batch_no, batch)| {
         attestation_sched_job(
@@ -265,6 +270,7 @@ async fn handle_attest(
             p2w_addr,
             config.clone(),
             Keypair::from_bytes(&payer.to_bytes()).unwrap(),
+            message_q_mtx.clone(),
         )
     });
 
@@ -337,6 +343,7 @@ async fn attestation_sched_job(
     p2w_addr: Pubkey,
     config: Pyth2WormholeConfig,
     payer: Keypair,
+    message_q_mtx: Arc<Mutex<P2WMessageQueue>>,
 ) -> Result<(), ErrBoxSend> {
     let mut retries_left = n_retries;
     // Enforces the max batch job count
@@ -357,6 +364,7 @@ async fn attestation_sched_job(
             Keypair::from_bytes(&payer.to_bytes()).unwrap(), // Keypair has no clone
             batch.symbols.to_vec(),
             sema.clone(),
+            message_q_mtx.clone(),
         );
 
         if daemon {
@@ -456,6 +464,7 @@ async fn attestation_job(
     payer: Keypair,
     symbols: Vec<P2WSymbol>,
     max_jobs_sema: Arc<Semaphore>,
+    message_q_mtx: Arc<Mutex<P2WMessageQueue>>,
 ) -> Result<(), ErrBoxSend> {
     // Will be dropped after attestation is complete
     let _permit = max_jobs_sema.acquire().await?;
@@ -470,17 +479,18 @@ async fn attestation_job(
         .map_err(|e| -> ErrBoxSend { e.into() })
         .await?;
 
+    let wh_msg_id = message_q_mtx.lock().await.get_account()?.id;
+
     let tx_res: Result<_, ErrBoxSend> = gen_attest_tx(
         p2w_addr,
         &config,
         &payer,
+        wh_msg_id,
         symbols.as_slice(),
-        &Keypair::new(),
         latest_blockhash,
     );
-    let tx = tx_res?;
     let sig = rpc
-        .send_and_confirm_transaction(&tx)
+        .send_and_confirm_transaction(&tx_res?)
         .map_err(|e| -> ErrBoxSend { e.into() })
         .await?;
     let tx_data = rpc

+ 134 - 0
solana/pyth2wormhole/client/src/message.rs

@@ -0,0 +1,134 @@
+//! Re-usable message scheme for pyth2wormhole
+
+use log::debug;
+use solana_program::system_instruction;
+use std::{
+    collections::VecDeque,
+    time::{
+        Duration,
+        Instant,
+    },
+};
+
+use crate::ErrBoxSend;
+
+/// One of the accounts tracked by the attestation client.
+#[derive(Clone, Debug)]
+pub struct P2WMessageAccount {
+    /// Unique ID that lets us derive unique accounts for use on-chain
+    pub id: u64,
+    /// Last time we've posted a message to wormhole with this account
+    pub last_used: Instant,
+}
+
+/// An umbrella data structure for tracking all message accounts in use
+#[derive(Clone, Debug)]
+pub struct P2WMessageQueue {
+    /// The tracked accounts. Sorted from oldest to newest, as guaranteed by get_account()
+    accounts: VecDeque<P2WMessageAccount>,
+    /// How much time needs to pass between reuses
+    grace_period: Duration,
+    /// A hard cap on how many accounts will be created.
+    max_accounts: usize,
+}
+
+impl P2WMessageQueue {
+    pub fn new(grace_period: Duration, max_accounts: usize) -> Self {
+        Self {
+            accounts: VecDeque::new(),
+            grace_period,
+            max_accounts
+        }
+    }
+    /// Finds or creates an account with last_used at least grace_period in the past.
+    ///
+    /// This method governs the self.accounts queue and preserves its sorted state.
+    pub fn get_account(&mut self) -> Result<P2WMessageAccount, ErrBoxSend> {
+        // Pick or add an account to use as message
+        let acc = match self.accounts.pop_front() {
+            // Exists and is old enough for reuse
+            Some(mut existing_acc) if existing_acc.last_used.elapsed() > self.grace_period => {
+                existing_acc.last_used = Instant::now();
+                existing_acc
+            }
+            // Exists but isn't old enough for reuse
+            Some(existing_too_new_acc) => {
+                // Counter-act the pop, this account is still oldest
+                // and will be old enough eventually.
+                self.accounts.push_front(existing_too_new_acc);
+
+                // Make sure we're not going over the limit
+                if self.accounts.len() >= self.max_accounts {
+                    return Err(format!("Max message queue size of {} reached.", self.max_accounts).into());
+                }
+
+                debug!(
+                    "Increasing message queue size to {}",
+                    self.accounts.len() + 1
+                );
+
+                // Use a new account instead
+                P2WMessageAccount {
+                    id: self.accounts.len() as u64,
+                    last_used: Instant::now(),
+                }
+            }
+            // Base case: Queue is empty, use a new account
+            None => P2WMessageAccount {
+                id: self.accounts.len() as u64,
+                last_used: Instant::now(),
+            },
+        };
+        // The chosen account becomes the newest, push it to the very end.
+        self.accounts.push_back(acc.clone());
+        Ok(acc)
+    }
+}
+
+pub mod test {
+    use super::*;
+
+    #[test]
+    fn test_empty_grows_only_as_needed() -> Result<(), ErrBoxSend> {
+        let mut q = P2WMessageQueue::new(Duration::from_millis(500), 100_000);
+
+        // Empty -> 1 account
+        let acc = q.get_account()?;
+
+        assert_eq!(q.accounts.len(), 1);
+        assert_eq!(acc.id, 0);
+
+        // 1 -> 2 accounts, not enough time passes
+        let acc2 = q.get_account()?;
+
+        assert_eq!(q.accounts.len(), 2);
+        assert_eq!(acc2.id, 1);
+
+        std::thread::sleep(Duration::from_millis(600));
+
+        // Account 0 should be in front, enough time passed 
+        let acc3 = q.get_account()?;
+
+        assert_eq!(q.accounts.len(), 2);
+        assert_eq!(acc3.id, 0);
+
+        // Account 1 also qualifies
+        let acc4 = q.get_account()?;
+
+        assert_eq!(q.accounts.len(), 2);
+        assert_eq!(acc4.id, 1);
+
+        // 2 -> 3 accounts, not enough time passes
+        let acc5 = q.get_account()?;
+
+        assert_eq!(q.accounts.len(), 3);
+        assert_eq!(acc5.id, 2);
+
+        // We should end up with 0, 1 and 2 in order
+        assert_eq!(q.accounts[0].id, 0);
+        assert_eq!(q.accounts[1].id, 1);
+        assert_eq!(q.accounts[2].id, 2);
+
+        Ok(())
+    }
+}

+ 3 - 2
solana/pyth2wormhole/client/tests/test_attest.rs

@@ -31,6 +31,8 @@ use solitaire::{
     BorshSerialize,
 };
 
+use std::time::Duration;
+
 use fixtures::{
     passthrough,
     pyth,
@@ -103,7 +105,6 @@ async fn test_happy_path() -> Result<(), p2wc::ErrBoxSend> {
     let (prod_id, price_id) = pyth::add_test_symbol(&mut p2w_test, &pyth_owner);
 
     let mut ctx = p2w_test.start_with_context().await;
-    let msg_keypair = Keypair::new();
 
     let symbols = vec![p2wc::P2WSymbol {
         name: Some("Mock symbol".to_owned()),
@@ -115,8 +116,8 @@ async fn test_happy_path() -> Result<(), p2wc::ErrBoxSend> {
         p2w_program_id,
         &p2w_config,
         &ctx.payer,
+        0,
         symbols.as_slice(),
-        &msg_keypair,
         ctx.last_blockhash,
     )?;
     ctx.banks_client.process_transaction(attest_tx).await?;

+ 19 - 23
solana/pyth2wormhole/client/tests/test_migrate.rs

@@ -27,8 +27,8 @@ use log::info;
 
 use pyth2wormhole::config::{
     OldP2WConfigAccount,
-    P2WConfigAccount,
     OldPyth2WormholeConfig,
+    P2WConfigAccount,
     Pyth2WormholeConfig,
 };
 use pyth2wormhole_client as p2wc;
@@ -55,7 +55,8 @@ async fn test_migrate_works() -> Result<(), solitaire::ErrBox> {
     let pyth_owner = Pubkey::new_unique();
 
     // On-chain state
-    let old_p2w_config = OldPyth2WormholeConfig {owner: p2w_owner.pubkey(),
+    let old_p2w_config = OldPyth2WormholeConfig {
+        owner: p2w_owner.pubkey(),
         wh_prog: wh_fixture_program_id,
         max_batch_size: pyth2wormhole::attest::P2W_MAX_BATCH_SIZE,
         pyth_owner,
@@ -79,8 +80,7 @@ async fn test_migrate_works() -> Result<(), solitaire::ErrBox> {
         executable: false,
         rent_epoch: 0,
     };
-    let old_p2w_config_addr =
-        OldP2WConfigAccount::key(None, &p2w_program_id);
+    let old_p2w_config_addr = OldP2WConfigAccount::key(None, &p2w_program_id);
 
     info!("Before add_account() calls");
 
@@ -94,12 +94,8 @@ async fn test_migrate_works() -> Result<(), solitaire::ErrBox> {
     info!("Before start_with_context");
     let mut ctx = p2w_test.start_with_context().await;
 
-    let migrate_tx = p2wc::gen_migrate_tx(
-        ctx.payer,
-        p2w_program_id,
-        p2w_owner,
-        ctx.last_blockhash,
-    )?;
+    let migrate_tx =
+        p2wc::gen_migrate_tx(ctx.payer, p2w_program_id, p2w_owner, ctx.last_blockhash)?;
     info!("Before process_transaction");
 
     // Migration should fail because the new config account is already initialized
@@ -108,7 +104,6 @@ async fn test_migrate_works() -> Result<(), solitaire::ErrBox> {
     Ok(())
 }
 
-
 #[tokio::test]
 async fn test_migrate_already_migrated() -> Result<(), solitaire::ErrBox> {
     info!("Starting");
@@ -121,13 +116,15 @@ async fn test_migrate_already_migrated() -> Result<(), solitaire::ErrBox> {
     let pyth_owner = Pubkey::new_unique();
 
     // On-chain state
-    let old_p2w_config = OldPyth2WormholeConfig {owner: p2w_owner.pubkey(),
+    let old_p2w_config = OldPyth2WormholeConfig {
+        owner: p2w_owner.pubkey(),
         wh_prog: wh_fixture_program_id,
         max_batch_size: pyth2wormhole::attest::P2W_MAX_BATCH_SIZE,
         pyth_owner,
     };
 
-    let new_p2w_config = Pyth2WormholeConfig {owner: p2w_owner.pubkey(),
+    let new_p2w_config = Pyth2WormholeConfig {
+        owner: p2w_owner.pubkey(),
         wh_prog: wh_fixture_program_id,
         max_batch_size: pyth2wormhole::attest::P2W_MAX_BATCH_SIZE,
         pyth_owner,
@@ -152,8 +149,7 @@ async fn test_migrate_already_migrated() -> Result<(), solitaire::ErrBox> {
         executable: false,
         rent_epoch: 0,
     };
-    let old_p2w_config_addr =
-        OldP2WConfigAccount::key(None, &p2w_program_id);
+    let old_p2w_config_addr = OldP2WConfigAccount::key(None, &p2w_program_id);
 
     let new_p2w_config_bytes = new_p2w_config.try_to_vec()?;
     let new_p2w_config_account = Account {
@@ -164,7 +160,7 @@ async fn test_migrate_already_migrated() -> Result<(), solitaire::ErrBox> {
         rent_epoch: 0,
     };
     let new_p2w_config_addr =
-        P2WConfigAccount::<{AccountState::Initialized}>::key(None, &p2w_program_id);
+        P2WConfigAccount::<{ AccountState::Initialized }>::key(None, &p2w_program_id);
 
     info!("Before add_account() calls");
 
@@ -174,16 +170,16 @@ async fn test_migrate_already_migrated() -> Result<(), solitaire::ErrBox> {
     info!("Before start_with_context");
     let mut ctx = p2w_test.start_with_context().await;
 
-    let migrate_tx = p2wc::gen_migrate_tx(
-        ctx.payer,
-        p2w_program_id,
-        p2w_owner,
-        ctx.last_blockhash,
-    )?;
+    let migrate_tx =
+        p2wc::gen_migrate_tx(ctx.payer, p2w_program_id, p2w_owner, ctx.last_blockhash)?;
     info!("Before process_transaction");
 
     // Migration should fail because the new config account is already initialized
-    assert!(ctx.banks_client.process_transaction(migrate_tx).await.is_err());
+    assert!(ctx
+        .banks_client
+        .process_transaction(migrate_tx)
+        .await
+        .is_err());
 
     Ok(())
 }

+ 107 - 33
solana/pyth2wormhole/program/src/attest.rs

@@ -1,4 +1,10 @@
-use crate::config::P2WConfigAccount;
+use crate::{
+    config::P2WConfigAccount,
+    message::{
+        P2WMessage,
+        P2WMessageDrvData,
+    },
+};
 use borsh::{
     BorshDeserialize,
     BorshSerialize,
@@ -20,9 +26,9 @@ use solana_program::{
 
 use p2w_sdk::{
     BatchPriceAttestation,
+    Identifier,
     P2WEmitter,
     PriceAttestation,
-    Identifier,
 };
 
 use bridge::{
@@ -102,7 +108,7 @@ pub struct Attest<'b> {
     /// Wormhole program address - must match the config value
     pub wh_prog: Info<'b>,
 
-    // wormhole's post_message accounts
+    // wormhole's post_message_unreliable accounts
     //
     // This contract makes no attempt to exhaustively validate
     // Wormhole's account inputs. Only the wormhole contract address
@@ -111,7 +117,7 @@ pub struct Attest<'b> {
     pub wh_bridge: Mut<Info<'b>>,
 
     /// Account to store the posted message
-    pub wh_message: Signer<Mut<Info<'b>>>,
+    pub wh_message: P2WMessage<'b>,
 
     /// Emitter of the VAA
     pub wh_emitter: P2WEmitter<'b>,
@@ -130,6 +136,7 @@ pub struct Attest<'b> {
 #[derive(BorshDeserialize, BorshSerialize)]
 pub struct AttestData {
     pub consistency_level: ConsistencyLevel,
+    pub message_account_id: u64,
 }
 
 pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> SoliResult<()> {
@@ -140,7 +147,14 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
         return Err(SolitaireError::Custom(4242));
     }
 
+    let wh_msg_drv_data = P2WMessageDrvData {
+        message_owner: accs.payer.key.clone(),
+        id: data.message_account_id,
+    };
+
     accs.config.verify_derivation(ctx.program_id, None)?;
+    accs.wh_message
+        .verify_derivation(ctx.program_id, &wh_msg_drv_data)?;
 
     if accs.config.wh_prog != *accs.wh_prog.key {
         trace!(&format!(
@@ -248,38 +262,98 @@ pub fn attest(ctx: &ExecutionContext, accs: &mut Attest, data: AttestData) -> So
     );
     solana_program::program::invoke(&transfer_ix, ctx.accounts)?;
 
-    // Send payload
-    let post_message_data = (
-        bridge::instruction::Instruction::PostMessage,
-        PostMessageData {
-            nonce: 0, // Superseded by the sequence number
-            payload: batch_attestation.serialize().map_err(|e| {
-                trace!(&e.to_string());
-                ProgramError::InvalidAccountData
-            })?,
-            consistency_level: data.consistency_level,
-        },
-    );
+    let payload = batch_attestation.serialize().map_err(|e| {
+        trace!(&e.to_string());
+        ProgramError::InvalidAccountData
+    })?;
 
-    let ix = Instruction::new_with_bytes(
-        accs.config.wh_prog,
-        post_message_data.try_to_vec()?.as_slice(),
-        vec![
-            AccountMeta::new(*accs.wh_bridge.key, false),
-            AccountMeta::new(*accs.wh_message.key, true),
-            AccountMeta::new_readonly(*accs.wh_emitter.key, true),
-            AccountMeta::new(*accs.wh_sequence.key, false),
-            AccountMeta::new(*accs.payer.key, true),
-            AccountMeta::new(*accs.wh_fee_collector.key, false),
-            AccountMeta::new_readonly(*accs.clock.info().key, false),
-            AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
-            AccountMeta::new_readonly(solana_program::system_program::id(), false),
-        ],
-    );
+    // Adjust message account size if necessary.
+    // NOTE: We assume that:
+    // - the rent and size values are far away from
+    // i64/u64/isize/usize overflow shenanigans (on the order of
+    // single kilobytes).
+    // - Pyth payload size change == Wormhole message size change (their metadata is constant-size)
+    if accs.wh_message.is_initialized() && accs.wh_message.payload.len() != payload.len() {
+        // NOTE: Payload =/= account size (account size includes
+        // surrounding wormhole data structure, payload is just the
+        // Pyth bytes).
+
+        // This value will be negative if we need to shrink down
+        let old_account_size = accs.wh_message.info().data_len();
 
-    trace!("Before cross-call");
+        // How much payload size changes
+        let payload_size_diff = payload.len() as isize - old_account_size as isize;
+
+        // How big the overall account data becomes
+        let new_account_size = (old_account_size as isize + payload_size_diff) as usize;
+
+        // Adjust account size
+        accs.wh_message.info().realloc(new_account_size, false)?;
+
+        // Exempt balance for adjusted size
+        let new_msg_account_balance = Rent::default().minimum_balance(new_account_size);
+
+        // How the account balance changes
+        let balance_diff =
+            new_msg_account_balance as i64 - accs.wh_message.info().lamports() as i64;
+
+        // How the diff affects payer balance
+        let new_payer_balance = (accs.payer.info().lamports() as i64 - balance_diff) as u64;
+
+        **accs.wh_message.info().lamports.borrow_mut() = new_msg_account_balance;
+        **accs.payer.info().lamports.borrow_mut() = new_payer_balance;
+
+        trace!("After message size/balance adjustment");
+    }
 
-    invoke_seeded(&ix, ctx, &accs.wh_emitter, None)?;
+    let ix = bridge::instructions::post_message_unreliable(
+        *accs.wh_prog.info().key,
+        *accs.payer.info().key,
+        *accs.wh_emitter.info().key,
+        *accs.wh_message.info().key,
+        0,
+        payload,
+        data.consistency_level.clone(),
+    )?;
+
+    trace!(&format!(
+        "Cross-call Seeds: {:?}",
+        [
+            // message seeds
+            P2WMessage::seeds(&wh_msg_drv_data)
+                .iter_mut()
+                .map(|seed| seed.as_slice())
+                .collect::<Vec<_>>()
+                .as_slice(),
+            // emitter seeds
+            P2WEmitter::seeds(None)
+                .iter_mut()
+                .map(|seed| seed.as_slice())
+                .collect::<Vec<_>>()
+                .as_slice(),
+        ]
+    ));
+
+    trace!("attest() finished, cross-calling wormhole");
+    invoke_signed(
+        &ix,
+        ctx.accounts,
+        [
+            // message seeds
+            P2WMessage::bumped_seeds(&wh_msg_drv_data, ctx.program_id)
+                .iter_mut()
+                .map(|seed| seed.as_slice())
+                .collect::<Vec<_>>()
+                .as_slice(),
+            // emitter seeds
+            P2WEmitter::bumped_seeds(None, ctx.program_id)
+                .iter_mut()
+                .map(|seed| seed.as_slice())
+                .collect::<Vec<_>>()
+                .as_slice(),
+        ]
+        .as_slice(),
+    )?;
 
     Ok(())
 }

+ 1 - 0
solana/pyth2wormhole/program/src/lib.rs

@@ -2,6 +2,7 @@
 pub mod attest;
 pub mod config;
 pub mod initialize;
+pub mod message;
 pub mod migrate;
 pub mod set_config;
 

+ 43 - 0
solana/pyth2wormhole/program/src/message.rs

@@ -0,0 +1,43 @@
+//! Index-based PDA for storing unreliable wormhole message
+//!
+//! The main goal of this PDA is to take advantage of wormhole message
+//! reuse securely. This is achieved by tying the account derivation
+//! data to the payer account of the attest() instruction. Inside
+//! attest(), payer must be a signer, and the message account must be
+//! derived with their address as message_owner in
+//! `P2WMessageDrvData`.
+
+use borsh::{
+    BorshDeserialize,
+    BorshSerialize,
+};
+use bridge::PostedMessageUnreliable;
+use solana_program::pubkey::Pubkey;
+use solitaire::{
+    processors::seeded::Seeded,
+    AccountState,
+    Data,
+    Info,
+    Mut,
+    Signer,
+};
+
+pub type P2WMessage<'a> = Mut<PostedMessageUnreliable<'a, { AccountState::MaybeInitialized }>>;
+
+#[derive(BorshDeserialize, BorshSerialize)]
+pub struct P2WMessageDrvData {
+    /// The key owning this message account
+    pub message_owner: Pubkey,
+    /// Index for keeping many accounts per owner
+    pub id: u64,
+}
+
+impl<'a> Seeded<&P2WMessageDrvData> for P2WMessage<'a> {
+    fn seeds(data: &P2WMessageDrvData) -> Vec<Vec<u8>> {
+        vec![
+            "p2w-message".as_bytes().to_vec(),
+            data.message_owner.to_bytes().to_vec(),
+            data.id.to_be_bytes().to_vec(),
+        ]
+    }
+}

+ 2 - 2
solana/pyth2wormhole/program/src/migrate.rs

@@ -70,7 +70,6 @@ pub fn migrate(ctx: &ExecutionContext, accs: &mut Migrate, data: ()) -> SoliResu
         ));
     }
 
-
     // Populate new config
     accs.new_config
         .create(ctx, accs.payer.info().key, CreationLamports::Exempt)?;
@@ -85,7 +84,8 @@ pub fn migrate(ctx: &ExecutionContext, accs: &mut Migrate, data: ()) -> SoliResu
     **accs.old_config.info().lamports.borrow_mut() = 0;
 
     // Credit payer with saved balance
-    let new_payer_balance = accs.payer
+    let new_payer_balance = accs
+        .payer
         .info()
         .lamports
         .borrow_mut()

+ 3 - 3
third_party/pyth/Dockerfile.p2w-attest

@@ -9,9 +9,9 @@ ADD third_party/pyth/p2w-sdk/rust /usr/src/third_party/pyth/p2w-sdk/rust
 RUN --mount=type=cache,target=/root/.cache \
     --mount=type=cache,target=target \
     --mount=type=cache,target=pyth2wormhole/target \
-    cargo build --manifest-path ./pyth2wormhole/Cargo.toml --package pyth2wormhole-client && \
-    cargo test --manifest-path ./pyth2wormhole/Cargo.toml --package pyth2wormhole-client && \
-    mv pyth2wormhole/target/debug/pyth2wormhole-client /usr/local/bin/pyth2wormhole-client && \
+    cargo build --package pyth2wormhole-client && \
+    cargo test --package pyth2wormhole-client && \
+    mv target/debug/pyth2wormhole-client /usr/local/bin/pyth2wormhole-client && \
     chmod a+rx /usr/src/pyth/*.py
 
 ENV P2W_OWNER_KEYPAIR="/usr/src/solana/keys/p2w_owner.json"