Pārlūkot izejas kodu

[accumulator-updater 7/x] - cpi max limit (#772)

* feat(accumulator-updater): add funding account for AccumulatorInput creation

* feat(accumulator-updater): add ix for testing cpi max size

add steps to notes.md on how to test against solana-test-validator with features deactivated

* test(accumulator-updater): fix max num of msgs to send

* refactor(accumulator-updater): address PR feedback from 771

update fund pda seeds, update consts, cleanup

* feat(accumulator-updater): address PR comments

Update cpi max test ix to take vec of msg sizes, clean up commented out code
swimricky 2 gadi atpakaļ
vecāks
revīzija
7b0851f9b8

+ 20 - 3
accumulator_updater/NOTES.md

@@ -1,12 +1,29 @@
+## Testing
+
+- run `anchor test` if no special customization for the test-validator is needed
+- `anchor test` will run `solana-test-validator` will all features activated.
+  One of the features activated on the test-validator which is not currently activated on pythnet is
+
+```
+"GDH5TVdbTPUpRnXaRyQqiKUa7uZAbZ28Q2N9bhbKoMLm  loosen cpi size restrictions #26641"
+```
+
+In order to run `solana-test-validator` with this feature deactivated, do the following:
+
+1. open a terminal and run `solana-test-validator --reset --deactivate-feature GDH5TVdbTPUpRnXaRyQqiKUa7uZAbZ28Q2N9bhbKoMLm`
+2. open a separate terminal and run `anchor build` in the `accumulator_updater` dir
+3. get the pubkeys of the program keypairs `solana address -k accumulator_updater/target/deploy/<program_keypair>.json`
+4. change the pubkeys in the `declare_id!` macro to these keypairs
+5. update `Anchor.toml` `[programs.localnet]` programIds as well
+6. run `anchor test --skip-local-validator`
+
 ## Questions
 
 1.  Do we need to support multiple Whitelists?
 2.  Support multiple accumulators
     1.  should each accumulator store a different type of data?
         => implications for length of merkle proof
-    2.
-3.  authority?
-4.  how to know what went into the `AccumulatorAccount` (for deserializing/proofs)
+3.  how to know what went into the `AccumulatorAccount` (for deserializing/proofs)
     1.  Header?
 
 ## To Do

+ 8 - 11
accumulator_updater/programs/accumulator_updater/src/instructions/put_all.rs

@@ -13,8 +13,9 @@ use {
 };
 
 
-pub const ACCUMULATOR: &[u8; 11] = b"accumulator";
-pub const FUND: &[u8; 4] = b"fund";
+pub const ACCUMULATOR: &str = "accumulator";
+pub const FUND: &str = "fund";
+
 
 pub fn put_all<'info>(
     ctx: Context<'_, '_, '_, 'info, PutAll<'info>>,
@@ -34,7 +35,7 @@ pub fn put_all<'info>(
             let (pda, bump) = Pubkey::find_program_address(
                 &[
                     cpi_caller.as_ref(),
-                    ACCUMULATOR.as_ref(),
+                    ACCUMULATOR.as_bytes(),
                     base_account_key.as_ref(),
                 ],
                 &crate::ID,
@@ -42,15 +43,15 @@ pub fn put_all<'info>(
             require_keys_eq!(accumulator_input_ai.key(), pda);
             let signer_seeds = [
                 cpi_caller.as_ref(),
-                ACCUMULATOR.as_ref(),
+                ACCUMULATOR.as_bytes(),
                 base_account_key.as_ref(),
                 &[bump],
             ];
             let fund_pda_bump = *ctx
                 .bumps
-                .get("fund")
+                .get(FUND)
                 .ok_or(AccumulatorUpdaterError::FundBumpNotFound)?;
-            let fund_signer_seeds = [ACCUMULATOR.as_ref(), FUND.as_ref(), &[fund_pda_bump]];
+            let fund_signer_seeds = [FUND.as_bytes(), &[fund_pda_bump]];
             PutAll::create_account(
                 accumulator_input_ai,
                 8 + AccumulatorInput::INIT_SPACE,
@@ -102,10 +103,7 @@ pub struct PutAll<'info> {
     /// `AccumulatorInput` account initialization
     #[account(
         mut,
-        seeds = [
-            b"accumulator".as_ref(),
-            b"fund".as_ref(),
-        ],
+        seeds = [b"fund".as_ref()],
         owner = system_program::System::id(),
         bump,
     )]
@@ -125,7 +123,6 @@ impl<'info> PutAll<'info> {
         system_program: &AccountInfo<'a>,
     ) -> Result<()> {
         let lamports = Rent::get()?.minimum_balance(space);
-
         system_program::create_account(
             CpiContext::new_with_signer(
                 system_program.to_account_info(),

+ 2 - 1
accumulator_updater/programs/accumulator_updater/src/state/whitelist.rs

@@ -47,7 +47,8 @@ pub struct WhitelistVerifier<'info> {
         seeds = [b"accumulator".as_ref(), b"whitelist".as_ref()],
         bump = whitelist.bump,
     )]
-    pub whitelist:  Account<'info, Whitelist>,
+    // Using a Box to move account from stack to heap
+    pub whitelist: Box<Account<'info, Whitelist>>,
     /// CHECK: Instruction introspection sysvar
     #[account(address = sysvar::instructions::ID)]
     pub ixs_sysvar: UncheckedAccount<'info>,

+ 37 - 0
accumulator_updater/programs/mock-cpi-caller/src/instructions/cpi_max_test.rs

@@ -0,0 +1,37 @@
+use {
+    crate::{
+        instructions::{
+            UpdatePrice,
+            UpdatePriceParams,
+        },
+        message::{
+            price::DummyPriceMessage,
+            AccumulatorSerializer,
+        },
+    },
+    anchor_lang::prelude::*,
+};
+
+pub fn cpi_max_test<'info>(
+    ctx: Context<'_, '_, '_, 'info, UpdatePrice<'info>>,
+    params: UpdatePriceParams,
+    msg_sizes: Vec<u16>,
+) -> Result<()> {
+    let mut inputs = vec![];
+
+    {
+        let pyth_price_acct = &mut ctx.accounts.pyth_price_account.load_mut()?;
+        pyth_price_acct.update(params)?;
+
+        for msg_size in msg_sizes {
+            let price_dummy_data = DummyPriceMessage::new(msg_size).accumulator_serialize()?;
+            inputs.push(price_dummy_data);
+        }
+    }
+
+    let input_len = inputs.iter().map(|x| x.len()).sum::<usize>();
+    msg!("input_len: {}", input_len);
+
+
+    UpdatePrice::emit_accumulator_inputs(ctx, inputs)
+}

+ 3 - 0
accumulator_updater/programs/mock-cpi-caller/src/instructions/mod.rs

@@ -1,9 +1,12 @@
 use anchor_lang::solana_program::hash::hashv;
 pub use {
     add_price::*,
+    cpi_max_test::*,
     update_price::*,
 };
+
 mod add_price;
+mod cpi_max_test;
 mod update_price;
 
 /// Generate discriminator to be able to call anchor program's ix

+ 1 - 8
accumulator_updater/programs/mock-cpi-caller/src/instructions/update_price.rs

@@ -5,17 +5,13 @@ use {
             ACCUMULATOR_UPDATER_IX_NAME,
         },
         message::{
-            get_schemas,
             price::{
                 CompactPriceMessage,
                 FullPriceMessage,
             },
             AccumulatorSerializer,
         },
-        state::{
-            PriceAccount,
-            PythAccountType,
-        },
+        state::PriceAccount,
     },
     accumulator_updater::program::AccumulatorUpdater as AccumulatorUpdaterProgram,
     anchor_lang::{
@@ -45,8 +41,6 @@ pub struct UpdatePrice<'info> {
     bump,
     )]
     pub pyth_price_account:    AccountLoader<'info, PriceAccount>,
-    // #[account(mut)]
-    // pub payer:                 Signer<'info>,
     #[account(mut)]
     pub fund:                  SystemAccount<'info>,
     /// Needed for accumulator_updater
@@ -66,7 +60,6 @@ pub fn update_price<'info>(
     params: UpdatePriceParams,
 ) -> Result<()> {
     let mut inputs = vec![];
-    let _schemas = get_schemas(PythAccountType::Price);
 
     {
         let pyth_price_acct = &mut ctx.accounts.pyth_price_account.load_mut()?;

+ 9 - 0
accumulator_updater/programs/mock-cpi-caller/src/lib.rs

@@ -28,6 +28,15 @@ pub mod mock_cpi_caller {
     ) -> Result<()> {
         instructions::update_price(ctx, params)
     }
+
+    /// num_messages is the number of 1kb messages to send to the CPI
+    pub fn cpi_max_test<'info>(
+        ctx: Context<'_, '_, '_, 'info, UpdatePrice<'info>>,
+        params: UpdatePriceParams,
+        msg_sizes: Vec<u16>,
+    ) -> Result<()> {
+        instructions::cpi_max_test(ctx, params, msg_sizes)
+    }
 }
 
 

+ 1 - 0
accumulator_updater/programs/mock-cpi-caller/src/message.rs

@@ -8,6 +8,7 @@ pub enum MessageSchema {
     Full    = 0,
     Compact = 1,
     Minimal = 2,
+    Dummy   = 3,
 }
 
 impl MessageSchema {

+ 32 - 0
accumulator_updater/programs/mock-cpi-caller/src/message/price.rs

@@ -113,3 +113,35 @@ impl AccumulatorSerializer for FullPriceMessage {
         Ok(bytes)
     }
 }
+
+
+#[repr(C)]
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct DummyPriceMessage {
+    pub header: MessageHeader,
+    pub data:   Vec<u8>,
+}
+
+
+impl DummyPriceMessage {
+    pub const SIZE: usize = 1017;
+
+    pub fn new(msg_size: u16) -> Self {
+        Self {
+            header: MessageHeader::new(MessageSchema::Dummy, msg_size as u32),
+            data:   vec![0u8; msg_size as usize],
+        }
+    }
+}
+
+
+impl AccumulatorSerializer for DummyPriceMessage {
+    fn accumulator_serialize(&self) -> Result<Vec<u8>> {
+        let mut bytes = vec![];
+        bytes.write_all(&self.header.schema.to_be_bytes())?;
+        bytes.write_all(&self.header.version.to_be_bytes())?;
+        bytes.write_all(&self.header.size.to_be_bytes())?;
+        bytes.extend_from_slice(&self.data);
+        Ok(bytes)
+    }
+}

+ 103 - 2
accumulator_updater/tests/accumulator_updater.ts

@@ -16,7 +16,7 @@ const accumulatorUpdaterProgram = anchor.workspace
 const mockCpiProg = anchor.workspace.MockCpiCaller as Program<MockCpiCaller>;
 let whitelistAuthority = anchor.web3.Keypair.generate();
 const [fundPda] = anchor.web3.PublicKey.findProgramAddressSync(
-  [Buffer.from("accumulator"), Buffer.from("fund")],
+  [Buffer.from("fund")],
   accumulatorUpdaterProgram.programId
 );
 
@@ -287,6 +287,107 @@ describe("accumulator_updater", () => {
       assert.isTrue(pm.priceExpo.eq(updatePriceParams.priceExpo));
     });
   });
+
+  it("Mock CPI Program - CPI Max Test", async () => {
+    // with loosen CPI feature activated, max cpi instruction size len is 10KB
+    let testCases = [[1024], [1024, 2048], [1024, 2048, 4096]];
+    // for (let i = 1; i < 8; i++) {
+    for (let i = 0; i < testCases.length; i++) {
+      let testCase = testCases[i];
+      console.info(`testCase: ${testCase}`);
+      const updatePriceParams = {
+        price: new anchor.BN(10 * i + 5),
+        priceExpo: new anchor.BN(10 & (i + 6)),
+        ema: new anchor.BN(10 * i + 7),
+        emaExpo: new anchor.BN(10 * i + 8),
+      };
+
+      let accumulatorPdaMeta = getAccumulatorPdaMeta(pythPriceAccountPk);
+      await mockCpiProg.methods
+        .cpiMaxTest(updatePriceParams, testCase)
+        .accounts({
+          fund: fundPda,
+          pythPriceAccount: pythPriceAccountPk,
+          ixsSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
+          accumulatorWhitelist: whitelistPubkey,
+          accumulatorProgram: accumulatorUpdaterProgram.programId,
+        })
+        .remainingAccounts([accumulatorPdaMeta])
+        .preInstructions([
+          ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
+        ])
+        .rpc({
+          skipPreflight: true,
+        });
+
+      const pythPriceAccount = await mockCpiProg.account.priceAccount.fetch(
+        pythPriceAccountPk
+      );
+      assert.isTrue(pythPriceAccount.price.eq(updatePriceParams.price));
+      assert.isTrue(pythPriceAccount.priceExpo.eq(updatePriceParams.priceExpo));
+      assert.isTrue(pythPriceAccount.ema.eq(updatePriceParams.ema));
+      assert.isTrue(pythPriceAccount.emaExpo.eq(updatePriceParams.emaExpo));
+      const accumulatorInput =
+        await accumulatorUpdaterProgram.account.accumulatorInput.fetch(
+          accumulatorPdaMeta.pubkey
+        );
+      const updatedAccumulatorPriceMessages =
+        parseAccumulatorInput(accumulatorInput);
+
+      console.log(
+        `updatedAccumulatorPriceMessages: ${JSON.stringify(
+          updatedAccumulatorPriceMessages,
+          null,
+          2
+        )}`
+      );
+      updatedAccumulatorPriceMessages.forEach((pm) => {
+        assert.isTrue(pm.id.eq(addPriceParams.id));
+        assert.isTrue(pm.price.eq(updatePriceParams.price));
+        assert.isTrue(pm.priceExpo.eq(updatePriceParams.priceExpo));
+      });
+    }
+  });
+
+  it("Mock CPI Program - CPI Max Test Fail", async () => {
+    // with loosen CPI feature activated, max cpi instruction size len is 10KB
+    let testCases = [[1024, 2048, 4096, 8192]];
+    // for (let i = 1; i < 8; i++) {
+    for (let i = 0; i < testCases.length; i++) {
+      let testCase = testCases[i];
+      console.info(`testCase: ${testCase}`);
+      const updatePriceParams = {
+        price: new anchor.BN(10 * i + 5),
+        priceExpo: new anchor.BN(10 & (i + 6)),
+        ema: new anchor.BN(10 * i + 7),
+        emaExpo: new anchor.BN(10 * i + 8),
+      };
+
+      let accumulatorPdaMeta = getAccumulatorPdaMeta(pythPriceAccountPk);
+      let errorThrown = false;
+      try {
+        await mockCpiProg.methods
+          .cpiMaxTest(updatePriceParams, testCase)
+          .accounts({
+            fund: fundPda,
+            pythPriceAccount: pythPriceAccountPk,
+            ixsSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY,
+            accumulatorWhitelist: whitelistPubkey,
+            accumulatorProgram: accumulatorUpdaterProgram.programId,
+          })
+          .remainingAccounts([accumulatorPdaMeta])
+          .preInstructions([
+            ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }),
+          ])
+          .rpc({
+            skipPreflight: true,
+          });
+      } catch (_err) {
+        errorThrown = true;
+      }
+      assert.ok(errorThrown);
+    }
+  });
 });
 
 export const getAccumulatorPdaMeta = (
@@ -339,7 +440,7 @@ function parseAccumulatorInput({
     } else if (msgHeader.schema == 1) {
       accumulatorMessages.push(parseCompactPriceMessage(msgData));
     } else {
-      console.warn("Unknown input index: " + i);
+      console.warn("unknown msgHeader.schema: " + i);
       continue;
     }
     start = endOffset;