Bläddra i källkod

feat(lazer): support ecdsa signers in solana contract (#2429)

Pavel Strakhov 8 månader sedan
förälder
incheckning
2efd5455ec

+ 2 - 18
lazer/Cargo.lock

@@ -3772,7 +3772,7 @@ dependencies = [
  "futures-util",
  "hex",
  "libsecp256k1 0.7.1",
- "pyth-lazer-protocol 0.6.1",
+ "pyth-lazer-protocol",
  "serde",
  "serde_json",
  "tokio",
@@ -3781,22 +3781,6 @@ dependencies = [
  "url",
 ]
 
-[[package]]
-name = "pyth-lazer-protocol"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "560a20d7f1040abad40245e524e657a2ec731e7e0560e25a1532301a1a04cb3f"
-dependencies = [
- "anyhow",
- "base64 0.22.1",
- "byteorder",
- "derive_more",
- "itertools 0.13.0",
- "rust_decimal",
- "serde",
- "serde_json",
-]
-
 [[package]]
 name = "pyth-lazer-protocol"
 version = "0.6.1"
@@ -3825,7 +3809,7 @@ dependencies = [
  "bytemuck",
  "byteorder",
  "hex",
- "pyth-lazer-protocol 0.5.0",
+ "pyth-lazer-protocol",
  "solana-program-test",
  "solana-sdk",
  "thiserror 2.0.3",

+ 1 - 1
lazer/contracts/solana/programs/pyth-lazer-solana-contract/Cargo.toml

@@ -19,7 +19,7 @@ no-log-ix-name = []
 idl-build = ["anchor-lang/idl-build"]
 
 [dependencies]
-pyth-lazer-protocol = "0.5.0"
+pyth-lazer-protocol = { path = "../../../../sdk/rust/protocol", version = "0.6.0" }
 
 anchor-lang = "0.30.1"
 bytemuck = "1.20.0"

+ 245 - 54
lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/lib.rs

@@ -2,7 +2,11 @@ mod signature;
 
 use {
     crate::signature::VerifiedMessage,
-    anchor_lang::{prelude::*, solana_program::pubkey::PUBKEY_BYTES, system_program},
+    anchor_lang::{
+        prelude::*,
+        solana_program::{keccak, pubkey::PUBKEY_BYTES, secp256k1_recover::secp256k1_recover},
+        system_program,
+    },
     std::mem::size_of,
 };
 
@@ -26,45 +30,81 @@ fn test_ids() {
 pub const ANCHOR_DISCRIMINATOR_BYTES: usize = 8;
 pub const MAX_NUM_TRUSTED_SIGNERS: usize = 2;
 pub const SPACE_FOR_TRUSTED_SIGNERS: usize = 5;
-pub const EXTRA_SPACE: usize = 100;
+pub const SPACE_FOR_TRUSTED_ECDSA_SIGNERS: usize = 2;
+pub const EXTRA_SPACE: usize = 43;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, AnchorSerialize, AnchorDeserialize)]
-pub struct TrustedSignerInfo {
-    pub pubkey: Pubkey,
+pub struct TrustedSignerInfo<T> {
+    pub pubkey: T,
     pub expires_at: i64,
 }
 
-impl TrustedSignerInfo {
-    const SERIALIZED_LEN: usize = PUBKEY_BYTES + size_of::<i64>();
-}
+pub const EVM_ADDRESS_LEN: usize = 20;
+pub type EvmAddress = [u8; EVM_ADDRESS_LEN];
 
 #[account]
+#[derive(Debug, PartialEq)]
 pub struct Storage {
     pub top_authority: Pubkey,
     pub treasury: Pubkey,
     pub single_update_fee_in_lamports: u64,
     pub num_trusted_signers: u8,
-    pub trusted_signers: [TrustedSignerInfo; SPACE_FOR_TRUSTED_SIGNERS],
+    pub trusted_signers: [TrustedSignerInfo<Pubkey>; SPACE_FOR_TRUSTED_SIGNERS],
+    pub num_trusted_ecdsa_signers: u8,
+    pub trusted_ecdsa_signers: [TrustedSignerInfo<EvmAddress>; SPACE_FOR_TRUSTED_ECDSA_SIGNERS],
     pub _extra_space: [u8; EXTRA_SPACE],
 }
 
+#[test]
+fn storage_size() {
+    // Keep the size the same when possible. If the size increases, we'll need to perform
+    // a migration that increases the account size on-chain.
+    assert_eq!(Storage::SERIALIZED_LEN, 373);
+}
+
 impl Storage {
     const SERIALIZED_LEN: usize = PUBKEY_BYTES
         + PUBKEY_BYTES
         + size_of::<u64>()
         + size_of::<u8>()
-        + TrustedSignerInfo::SERIALIZED_LEN * SPACE_FOR_TRUSTED_SIGNERS
+        + (PUBKEY_BYTES + size_of::<i64>()) * SPACE_FOR_TRUSTED_SIGNERS
+        + size_of::<u8>()
+        + (EVM_ADDRESS_LEN + size_of::<i64>()) * SPACE_FOR_TRUSTED_ECDSA_SIGNERS
         + EXTRA_SPACE;
 
-    pub fn initialized_trusted_signers(&self) -> &[TrustedSignerInfo] {
+    pub fn initialized_trusted_signers(&self) -> &[TrustedSignerInfo<Pubkey>] {
         &self.trusted_signers[0..usize::from(self.num_trusted_signers)]
     }
+
+    pub fn initialized_trusted_ecdsa_signers(&self) -> &[TrustedSignerInfo<EvmAddress>] {
+        &self.trusted_ecdsa_signers[0..usize::from(self.num_trusted_ecdsa_signers)]
+    }
+
+    pub fn is_trusted(&self, signer: &Pubkey) -> std::result::Result<bool, ProgramError> {
+        let now = Clock::get()?.unix_timestamp;
+
+        Ok(self
+            .initialized_trusted_signers()
+            .iter()
+            .any(|s| &s.pubkey == signer && s.expires_at > now))
+    }
+
+    pub fn is_ecdsa_trusted(&self, signer: &EvmAddress) -> std::result::Result<bool, ProgramError> {
+        let now = Clock::get()?.unix_timestamp;
+
+        Ok(self
+            .initialized_trusted_ecdsa_signers()
+            .iter()
+            .any(|s| &s.pubkey == signer && s.expires_at > now))
+    }
 }
 
 pub const STORAGE_SEED: &[u8] = b"storage";
 
 #[program]
 pub mod pyth_lazer_solana_contract {
+    use pyth_lazer_protocol::message::LeEcdsaMessage;
+
     use super::*;
 
     pub fn initialize(
@@ -79,51 +119,27 @@ pub mod pyth_lazer_solana_contract {
     }
 
     pub fn update(ctx: Context<Update>, trusted_signer: Pubkey, expires_at: i64) -> Result<()> {
-        let num_trusted_signers: usize = ctx.accounts.storage.num_trusted_signers.into();
-        if num_trusted_signers > ctx.accounts.storage.trusted_signers.len() {
-            return Err(ProgramError::InvalidAccountData.into());
-        }
-        if num_trusted_signers > MAX_NUM_TRUSTED_SIGNERS {
-            return Err(ProgramError::InvalidAccountData.into());
-        }
-        let mut trusted_signers =
-            ctx.accounts.storage.trusted_signers[..num_trusted_signers].to_vec();
-        if expires_at == 0 {
-            // Delete
-            let pos = trusted_signers
-                .iter()
-                .position(|item| item.pubkey == trusted_signer)
-                .ok_or(ProgramError::InvalidInstructionData)?;
-            trusted_signers.remove(pos);
-        } else if let Some(item) = trusted_signers
-            .iter_mut()
-            .find(|item| item.pubkey == trusted_signer)
-        {
-            // Modify
-            item.expires_at = expires_at;
-        } else {
-            // Add
-            trusted_signers.push(TrustedSignerInfo {
-                pubkey: trusted_signer,
-                expires_at,
-            });
-        }
-
-        if trusted_signers.len() > ctx.accounts.storage.trusted_signers.len() {
-            return Err(ProgramError::AccountDataTooSmall.into());
-        }
-        if trusted_signers.len() > MAX_NUM_TRUSTED_SIGNERS {
-            return Err(ProgramError::InvalidInstructionData.into());
-        }
+        let storage = &mut *ctx.accounts.storage;
+        update_trusted_signer(
+            &mut storage.num_trusted_signers,
+            &mut storage.trusted_signers,
+            trusted_signer,
+            expires_at,
+        )
+    }
 
-        ctx.accounts.storage.trusted_signers = Default::default();
-        ctx.accounts.storage.trusted_signers[..trusted_signers.len()]
-            .copy_from_slice(&trusted_signers);
-        ctx.accounts.storage.num_trusted_signers = trusted_signers
-            .len()
-            .try_into()
-            .expect("num signers overflow");
-        Ok(())
+    pub fn update_ecdsa_signer(
+        ctx: Context<Update>,
+        trusted_signer: EvmAddress,
+        expires_at: i64,
+    ) -> Result<()> {
+        let storage = &mut *ctx.accounts.storage;
+        update_trusted_signer(
+            &mut storage.num_trusted_ecdsa_signers,
+            &mut storage.trusted_ecdsa_signers,
+            trusted_signer,
+            expires_at,
+        )
     }
 
     /// Verifies a ed25519 signature on Solana by checking that the transaction contains
@@ -164,6 +180,47 @@ pub mod pyth_lazer_solana_contract {
             err.into()
         })
     }
+
+    pub fn verify_ecdsa_message(
+        ctx: Context<VerifyEcdsaMessage>,
+        message_data: Vec<u8>,
+    ) -> Result<()> {
+        system_program::transfer(
+            CpiContext::new(
+                ctx.accounts.system_program.to_account_info(),
+                system_program::Transfer {
+                    from: ctx.accounts.payer.to_account_info(),
+                    to: ctx.accounts.treasury.to_account_info(),
+                },
+            ),
+            ctx.accounts.storage.single_update_fee_in_lamports,
+        )?;
+
+        let message = LeEcdsaMessage::deserialize_slice(&message_data)
+            .map_err(|_| ProgramError::InvalidInstructionData)?;
+
+        let pubkey = secp256k1_recover(
+            &keccak::hash(&message.payload).0,
+            message.recovery_id,
+            &message.signature,
+        )
+        .map_err(|err| {
+            msg!("secp256k1_recover failed: {:?}", err);
+            ProgramError::InvalidInstructionData
+        })?;
+        let addr: EvmAddress = keccak::hash(&pubkey.0).0[12..]
+            .try_into()
+            .expect("invalid addr len");
+        if addr == EvmAddress::default() {
+            msg!("secp256k1_recover failed: zero output");
+            return Err(ProgramError::InvalidInstructionData.into());
+        }
+        if !ctx.accounts.storage.is_ecdsa_trusted(&addr)? {
+            msg!("untrusted signer: {:?}", addr);
+            return Err(ProgramError::MissingRequiredSignature.into());
+        }
+        Ok(())
+    }
 }
 
 #[derive(Accounts)]
@@ -211,3 +268,137 @@ pub struct VerifyMessage<'info> {
     /// This account is not usable with anchor's `Program` account type because it's not executable.
     pub instructions_sysvar: AccountInfo<'info>,
 }
+
+#[derive(Accounts)]
+pub struct VerifyEcdsaMessage<'info> {
+    #[account(mut)]
+    pub payer: Signer<'info>,
+    #[account(
+        seeds = [STORAGE_SEED],
+        bump,
+        has_one = treasury
+    )]
+    pub storage: Account<'info, Storage>,
+    /// CHECK: this account doesn't need additional constraints.
+    pub treasury: AccountInfo<'info>,
+    pub system_program: Program<'info, System>,
+}
+
+fn update_trusted_signer<T: Copy + PartialEq + Default>(
+    stored_num_trusted_signers: &mut u8,
+    stored_trusted_signers: &mut [TrustedSignerInfo<T>],
+    trusted_signer: T,
+    expires_at: i64,
+) -> Result<()> {
+    let num_trusted_signers: usize = (*stored_num_trusted_signers).into();
+    if num_trusted_signers > stored_trusted_signers.len() {
+        return Err(ProgramError::InvalidAccountData.into());
+    }
+    if num_trusted_signers > MAX_NUM_TRUSTED_SIGNERS {
+        return Err(ProgramError::InvalidAccountData.into());
+    }
+    let mut trusted_signers = stored_trusted_signers[..num_trusted_signers].to_vec();
+    if expires_at == 0 {
+        // Delete
+        let pos = trusted_signers
+            .iter()
+            .position(|item| item.pubkey == trusted_signer)
+            .ok_or(ProgramError::InvalidInstructionData)?;
+        trusted_signers.remove(pos);
+    } else if let Some(item) = trusted_signers
+        .iter_mut()
+        .find(|item| item.pubkey == trusted_signer)
+    {
+        // Modify
+        item.expires_at = expires_at;
+    } else {
+        // Add
+        trusted_signers.push(TrustedSignerInfo {
+            pubkey: trusted_signer,
+            expires_at,
+        });
+    }
+
+    if trusted_signers.len() > trusted_signers.len() {
+        return Err(ProgramError::AccountDataTooSmall.into());
+    }
+    if trusted_signers.len() > MAX_NUM_TRUSTED_SIGNERS {
+        return Err(ProgramError::InvalidInstructionData.into());
+    }
+
+    stored_trusted_signers[..trusted_signers.len()].copy_from_slice(&trusted_signers);
+    for item in &mut stored_trusted_signers[trusted_signers.len()..] {
+        *item = Default::default();
+    }
+    *stored_num_trusted_signers = trusted_signers
+        .len()
+        .try_into()
+        .expect("num signers overflow");
+    Ok(())
+}
+
+#[test]
+fn test_storage_compat_after_adding_ecdsa() {
+    // This is data of a storage account created by the previous version of the contract.
+    let data = [
+        209, 117, 255, 185, 196, 175, 68, 9, 221, 56, 75, 202, 174, 248, 122, 155, 212, 29, 112,
+        50, 82, 65, 161, 137, 16, 164, 61, 134, 119, 132, 149, 1, 178, 177, 3, 187, 25, 187, 143,
+        244, 233, 140, 161, 230, 115, 255, 214, 103, 208, 40, 16, 101, 45, 35, 153, 15, 145, 134,
+        250, 244, 248, 255, 51, 165, 169, 186, 183, 210, 155, 137, 30, 84, 1, 0, 0, 0, 0, 0, 0, 0,
+        1, 116, 49, 58, 101, 37, 237, 249, 153, 54, 170, 20, 119, 233, 76, 114, 188, 92, 198, 23,
+        178, 23, 69, 245, 240, 50, 150, 243, 21, 68, 97, 242, 20, 255, 255, 255, 255, 255, 255,
+        255, 127, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    ];
+    let storage = Storage::deserialize(&mut &data[..]).unwrap();
+    assert_eq!(
+        storage,
+        Storage {
+            top_authority: pubkey!("F6eZvgfuPtncCUDzYgzaFPRodHwZXQHe1pC4kkyvkYwa"),
+            treasury: pubkey!("D2Y884NqR9TVagZftdzzuEgtTEwd3AsS2nLMHEnVkXCQ"),
+            single_update_fee_in_lamports: 6061433450835458729,
+            num_trusted_signers: 1,
+            trusted_signers: [
+                TrustedSignerInfo {
+                    pubkey: pubkey!("1111111avyLnoUfmuX6KZaaTrSfth7n9tX4u4rVV"),
+                    expires_at: 1509375770176493106
+                },
+                TrustedSignerInfo {
+                    pubkey: pubkey!("JEKNVnkbo2qryGmQn1b2RCJcGKVCn6WvNZmFdEiZGVSo"),
+                    expires_at: 0
+                },
+                TrustedSignerInfo {
+                    pubkey: Pubkey::default(),
+                    expires_at: 0
+                },
+                TrustedSignerInfo {
+                    pubkey: Pubkey::default(),
+                    expires_at: 0
+                },
+                TrustedSignerInfo {
+                    pubkey: Pubkey::default(),
+                    expires_at: 0
+                }
+            ],
+            num_trusted_ecdsa_signers: 0,
+            trusted_ecdsa_signers: [
+                TrustedSignerInfo {
+                    pubkey: Default::default(),
+                    expires_at: 0
+                },
+                TrustedSignerInfo {
+                    pubkey: Default::default(),
+                    expires_at: 0
+                },
+            ],
+            _extra_space: [0; 43],
+        }
+    );
+}

+ 6 - 10
lazer/contracts/solana/programs/pyth-lazer-solana-contract/src/signature.rs

@@ -1,7 +1,7 @@
 use {
     crate::Storage,
     anchor_lang::{
-        prelude::{borsh, AccountInfo, Clock, ProgramError, Pubkey, SolanaSysvar},
+        prelude::{borsh, AccountInfo, ProgramError, Pubkey},
         solana_program::{
             ed25519_program, program_memory::sol_memcmp, pubkey::PUBKEY_BYTES, sysvar,
         },
@@ -9,6 +9,7 @@ use {
     },
     bytemuck::{cast_slice, checked::try_cast_slice, Pod, Zeroable},
     byteorder::{ByteOrder, LE},
+    pyth_lazer_protocol::message::format_magics_le::SOLANA_FORMAT_MAGIC,
     thiserror::Error,
 };
 
@@ -172,8 +173,6 @@ pub fn verify_message(
     ed25519_instruction_index: u16,
     signature_index: u8,
 ) -> Result<VerifiedMessage, SignatureVerificationError> {
-    const SOLANA_FORMAT_MAGIC_LE: u32 = 2182742457;
-
     let self_instruction_index =
         sysvar::instructions::load_current_index_checked(instructions_sysvar)
             .map_err(SignatureVerificationError::LoadCurrentIndexFailed)?;
@@ -237,7 +236,7 @@ pub fn verify_message(
     }
 
     let magic = LE::read_u32(&message_data[..MAGIC_LEN.into()]);
-    if magic != SOLANA_FORMAT_MAGIC_LE {
+    if magic != SOLANA_FORMAT_MAGIC {
         return Err(SignatureVerificationError::FormatMagicMismatch);
     }
 
@@ -294,13 +293,10 @@ pub fn verify_message(
             .ok_or(SignatureVerificationError::MessageOffsetOverflow)?;
         &message_data[start..end]
     };
-    let now = Clock::get()
-        .map_err(SignatureVerificationError::ClockGetFailed)?
-        .unix_timestamp;
+
     if !storage
-        .initialized_trusted_signers()
-        .iter()
-        .any(|s| s.pubkey.as_ref() == public_key && s.expires_at > now)
+        .is_trusted(&public_key.try_into().expect("invalid pubkey len"))
+        .map_err(SignatureVerificationError::ClockGetFailed)?
     {
         return Err(SignatureVerificationError::NotTrustedSigner);
     }

+ 127 - 93
lazer/contracts/solana/programs/pyth-lazer-solana-contract/tests/test1.rs

@@ -1,6 +1,6 @@
 use {
     anchor_lang::{prelude::AccountMeta, InstructionData},
-    pyth_lazer_solana_contract::ed25519_program_args,
+    pyth_lazer_solana_contract::{ed25519_program_args, EvmAddress},
     solana_program_test::{BanksClient, BanksClientError, ProgramTest},
     solana_sdk::{
         ed25519_program,
@@ -37,31 +37,32 @@ struct Setup {
     banks_client: BanksClient,
     payer: Keypair,
     recent_blockhash: Hash,
+    treasury: Pubkey,
 }
 
 impl Setup {
     async fn with_program_test(program_test: ProgramTest) -> Self {
         let (banks_client, payer, recent_blockhash) = program_test.start().await;
-        Self {
+        let mut setup = Self {
+            treasury: Pubkey::create_with_seed(&payer.pubkey(), "treasury", &system_program::ID)
+                .unwrap(),
             banks_client,
             payer,
             recent_blockhash,
-        }
+        };
+        setup.create_treasury().await;
+        setup
     }
 
     async fn new() -> Self {
         Self::with_program_test(program_test()).await
     }
 
-    async fn create_treasury(&mut self) -> Pubkey {
-        let treasury =
-            Pubkey::create_with_seed(&self.payer.pubkey(), "treasury", &system_program::ID)
-                .unwrap();
-
+    async fn create_treasury(&mut self) {
         let mut transaction_create_treasury = Transaction::new_with_payer(
             &[system_instruction::create_account_with_seed(
                 &self.payer.pubkey(),
-                &treasury,
+                &self.treasury,
                 &self.payer.pubkey(),
                 "treasury",
                 10_000_000,
@@ -75,7 +76,30 @@ impl Setup {
             .process_transaction(transaction_create_treasury)
             .await
             .unwrap();
-        treasury
+    }
+
+    async fn init_contract(&mut self) {
+        let mut transaction_init_contract = Transaction::new_with_payer(
+            &[Instruction::new_with_bytes(
+                pyth_lazer_solana_contract::ID,
+                &pyth_lazer_solana_contract::instruction::Initialize {
+                    top_authority: self.payer.pubkey(),
+                    treasury: self.treasury,
+                }
+                .data(),
+                vec![
+                    AccountMeta::new(self.payer.pubkey(), true),
+                    AccountMeta::new(pyth_lazer_solana_contract::STORAGE_ID, false),
+                    AccountMeta::new_readonly(system_program::ID, false),
+                ],
+            )],
+            Some(&self.payer.pubkey()),
+        );
+        transaction_init_contract.sign(&[&self.payer], self.recent_blockhash);
+        self.banks_client
+            .process_transaction(transaction_init_contract)
+            .await
+            .unwrap();
     }
 
     async fn set_trusted(&mut self, verifying_key: Pubkey) {
@@ -101,23 +125,44 @@ impl Setup {
             .unwrap();
     }
 
-    async fn verify_message(&mut self, message: &[u8], treasury: Pubkey) {
+    async fn set_trusted_ecdsa(&mut self, verifying_key: EvmAddress) {
+        let mut transaction_set_trusted = Transaction::new_with_payer(
+            &[Instruction::new_with_bytes(
+                pyth_lazer_solana_contract::ID,
+                &pyth_lazer_solana_contract::instruction::UpdateEcdsaSigner {
+                    trusted_signer: verifying_key,
+                    expires_at: i64::MAX,
+                }
+                .data(),
+                vec![
+                    AccountMeta::new(self.payer.pubkey(), true),
+                    AccountMeta::new(pyth_lazer_solana_contract::STORAGE_ID, false),
+                ],
+            )],
+            Some(&self.payer.pubkey()),
+        );
+        transaction_set_trusted.sign(&[&self.payer], self.recent_blockhash);
+        self.banks_client
+            .process_transaction(transaction_set_trusted)
+            .await
+            .unwrap();
+    }
+
+    async fn verify_message(&mut self, message: &[u8]) {
         let treasury_starting_lamports = self
             .banks_client
-            .get_account(treasury)
+            .get_account(self.treasury)
             .await
             .unwrap()
             .unwrap()
             .lamports;
 
         // 8 bytes for Anchor header, 4 bytes for Vec length.
-        self.verify_message_with_offset(message, treasury, 12)
-            .await
-            .unwrap();
+        self.verify_message_with_offset(message, 12).await.unwrap();
 
         assert_eq!(
             self.banks_client
-                .get_account(treasury)
+                .get_account(self.treasury)
                 .await
                 .unwrap()
                 .unwrap()
@@ -129,7 +174,6 @@ impl Setup {
     async fn verify_message_with_offset(
         &mut self,
         message: &[u8],
-        treasury: Pubkey,
         message_offset: u16,
     ) -> Result<(), BanksClientError> {
         // Instruction #0 will be ed25519 instruction;
@@ -159,7 +203,7 @@ impl Setup {
                     vec![
                         AccountMeta::new(self.payer.pubkey(), true),
                         AccountMeta::new_readonly(pyth_lazer_solana_contract::STORAGE_ID, false),
-                        AccountMeta::new(treasury, false),
+                        AccountMeta::new(self.treasury, false),
                         AccountMeta::new_readonly(system_program::ID, false),
                         AccountMeta::new_readonly(sysvar::instructions::ID, false),
                     ],
@@ -172,35 +216,54 @@ impl Setup {
             .process_transaction(transaction_verify)
             .await
     }
+
+    async fn verify_message_ecdsa(&mut self, message: &[u8]) {
+        let treasury_starting_lamports = self
+            .banks_client
+            .get_account(self.treasury)
+            .await
+            .unwrap()
+            .unwrap()
+            .lamports;
+
+        let mut transaction_verify = Transaction::new_with_payer(
+            &[Instruction::new_with_bytes(
+                pyth_lazer_solana_contract::ID,
+                &pyth_lazer_solana_contract::instruction::VerifyEcdsaMessage {
+                    message_data: message.to_vec(),
+                }
+                .data(),
+                vec![
+                    AccountMeta::new(self.payer.pubkey(), true),
+                    AccountMeta::new_readonly(pyth_lazer_solana_contract::STORAGE_ID, false),
+                    AccountMeta::new(self.treasury, false),
+                    AccountMeta::new_readonly(system_program::ID, false),
+                ],
+            )],
+            Some(&self.payer.pubkey()),
+        );
+        transaction_verify.sign(&[&self.payer], self.recent_blockhash);
+        self.banks_client
+            .process_transaction(transaction_verify)
+            .await
+            .unwrap();
+
+        assert_eq!(
+            self.banks_client
+                .get_account(self.treasury)
+                .await
+                .unwrap()
+                .unwrap()
+                .lamports,
+            treasury_starting_lamports + 1,
+        );
+    }
 }
 
 #[tokio::test]
 async fn test_basic() {
     let mut setup = Setup::new().await;
-    let treasury = setup.create_treasury().await;
-
-    let mut transaction_init_contract = Transaction::new_with_payer(
-        &[Instruction::new_with_bytes(
-            pyth_lazer_solana_contract::ID,
-            &pyth_lazer_solana_contract::instruction::Initialize {
-                top_authority: setup.payer.pubkey(),
-                treasury,
-            }
-            .data(),
-            vec![
-                AccountMeta::new(setup.payer.pubkey(), true),
-                AccountMeta::new(pyth_lazer_solana_contract::STORAGE_ID, false),
-                AccountMeta::new_readonly(system_program::ID, false),
-            ],
-        )],
-        Some(&setup.payer.pubkey()),
-    );
-    transaction_init_contract.sign(&[&setup.payer], setup.recent_blockhash);
-    setup
-        .banks_client
-        .process_transaction(transaction_init_contract)
-        .await
-        .unwrap();
+    setup.init_contract().await;
 
     let verifying_key =
         hex::decode("74313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f214").unwrap();
@@ -212,36 +275,13 @@ async fn test_basic() {
     .unwrap();
 
     setup.set_trusted(verifying_key.try_into().unwrap()).await;
-    setup.verify_message(&message, treasury).await;
+    setup.verify_message(&message).await;
 }
 
 #[tokio::test]
 async fn test_alignment() {
     let mut setup = Setup::new().await;
-    let treasury = setup.create_treasury().await;
-
-    let mut transaction_init_contract = Transaction::new_with_payer(
-        &[Instruction::new_with_bytes(
-            pyth_lazer_solana_contract::ID,
-            &pyth_lazer_solana_contract::instruction::Initialize {
-                top_authority: setup.payer.pubkey(),
-                treasury,
-            }
-            .data(),
-            vec![
-                AccountMeta::new(setup.payer.pubkey(), true),
-                AccountMeta::new(pyth_lazer_solana_contract::STORAGE_ID, false),
-                AccountMeta::new_readonly(system_program::ID, false),
-            ],
-        )],
-        Some(&setup.payer.pubkey()),
-    );
-    transaction_init_contract.sign(&[&setup.payer], setup.recent_blockhash);
-    setup
-        .banks_client
-        .process_transaction(transaction_init_contract)
-        .await
-        .unwrap();
+    setup.init_contract().await;
 
     let verifying_key =
         hex::decode("f65210bee4fcf5b1cee1e537fabcfd95010297653b94af04d454fc473e94834f").unwrap();
@@ -255,36 +295,13 @@ async fn test_alignment() {
     .unwrap();
 
     setup.set_trusted(verifying_key.try_into().unwrap()).await;
-    setup.verify_message(&message, treasury).await;
+    setup.verify_message(&message).await;
 }
 
 #[tokio::test]
 async fn test_rejects_wrong_offset() {
     let mut setup = Setup::new().await;
-    let treasury = setup.create_treasury().await;
-
-    let mut transaction_init_contract = Transaction::new_with_payer(
-        &[Instruction::new_with_bytes(
-            pyth_lazer_solana_contract::ID,
-            &pyth_lazer_solana_contract::instruction::Initialize {
-                top_authority: setup.payer.pubkey(),
-                treasury,
-            }
-            .data(),
-            vec![
-                AccountMeta::new(setup.payer.pubkey(), true),
-                AccountMeta::new(pyth_lazer_solana_contract::STORAGE_ID, false),
-                AccountMeta::new_readonly(system_program::ID, false),
-            ],
-        )],
-        Some(&setup.payer.pubkey()),
-    );
-    transaction_init_contract.sign(&[&setup.payer], setup.recent_blockhash);
-    setup
-        .banks_client
-        .process_transaction(transaction_init_contract)
-        .await
-        .unwrap();
+    setup.init_contract().await;
 
     let verifying_key =
         hex::decode("74313a6525edf99936aa1477e94c72bc5cc617b21745f5f03296f3154461f214").unwrap();
@@ -324,7 +341,7 @@ async fn test_rejects_wrong_offset() {
     setup.set_trusted(verifying_key.try_into().unwrap()).await;
     setup.set_trusted(verifying_key_2.try_into().unwrap()).await;
     let err = setup
-        .verify_message_with_offset(&message, treasury, 12 + 130)
+        .verify_message_with_offset(&message, 12 + 130)
         .await
         .unwrap_err();
     assert!(matches!(
@@ -335,3 +352,20 @@ async fn test_rejects_wrong_offset() {
         ))
     ));
 }
+
+#[tokio::test]
+async fn test_ecdsa() {
+    let mut setup = Setup::new().await;
+    setup.init_contract().await;
+
+    let verifying_key = hex::decode("b8d50f0bae75bf6e03c104903d7c3afc4a6596da").unwrap();
+    let message = hex::decode(
+        "e4bd474dd4b822eca4509650613e58b21db858e60750ab3498d4a484028785981740adf42cd558bb4f9efd5157bcbb60a1939470ead091b82b63641ad962c7a537db4eb300310075d3c793c0afe900e42e060003010100000004000054e616b201000004f8ff020035dc1cb2010000010073f010b2010000",
+    )
+    .unwrap();
+
+    setup
+        .set_trusted_ecdsa(verifying_key.try_into().unwrap())
+        .await;
+    setup.verify_message_ecdsa(&message).await;
+}