hopper-runtime 0.4.5

Canonical low-level runtime surface for Hopper programs: direct account memory, validation, borrow guards, CPI, and zero-copy state access.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
//! Stack-allocated variable-length CPI builder.
//!
//! The `hopper_runtime::cpi::invoke_signed::<N>` family is const-generic over
//! the account count and serves CPI shapes known at compile time. [`DynCpi`]
//! covers shapes whose account count or data length is determined while the
//! instruction is being built, including:
//!
//! - Aggregators that invoke the same program with a runtime-
//!   decided account count (fanout fee routers, batch settlement
//!   cranks).
//! - Forwarders that pass through the caller's remaining accounts
//!   after splicing in a known prefix.
//! - Generic instruction builders that construct the data buffer
//!   byte-by-byte from user input (priority-fee overrides, optional
//!   bump seeds) and do not know the final length until build time.
//!
//! The builder has compile-time capacities, `MAX_ACCTS` and `MAX_DATA`, and
//! stores both buffers inline. [`DynCpi::push_account`] and
//! [`DynCpi::push_data`] return `ProgramError::InvalidArgument` before a write
//! that would exceed those capacities. Account insertion preserves the ordered
//! meta list while maintaining a pubkey-deduplicated account-info projection.
//! [`DynCpi::invoke_signed`] accepts Hopper's typed [`Signer`] values for PDA
//! signer seeds.

use core::mem::MaybeUninit;

use crate::instruction::{InstructionAccount, InstructionView, Signer};
use crate::{
    account::AccountView,
    address::{address_eq, Address},
    error::ProgramError,
    result::ProgramResult,
};

/// Variable-length CPI builder with compile-time stack capacity.
///
/// `MAX_ACCTS` is the upper bound on the number of `AccountMeta`
/// entries. `MAX_DATA` is the upper bound on the instruction data
/// byte count. Exceeding either returns an error; nothing panics.
///
/// Use when the CPI shape is not known at compile time. For
/// statically-shaped CPIs, prefer `cpi::invoke_signed::<N>` which
/// avoids the two bounds entirely.
pub struct DynCpi<'a, const MAX_ACCTS: usize, const MAX_DATA: usize> {
    program_id: &'a Address,
    // Per-push (meta) storage: one slot per `push_account`, order and
    // duplicates preserved; this is the ordered meta list the callee sees.
    accounts: [MaybeUninit<&'a AccountView<'a>>; MAX_ACCTS],
    writable: [bool; MAX_ACCTS],
    signer: [bool; MAX_ACCTS],
    account_count: usize,
    // SIMD-0339 dedup projection (by pubkey): one entry per *unique*
    // account. `info_first[k]` is the push index of the first occurrence of
    // unique account `k` (used to recover its `AccountView`); the writable /
    // signer flags are the OR across every occurrence, because an account
    // that is writable (or a signer) in *any* meta must be passed to the
    // syscall as writable (or signer). `u16` suffices: MAX_ACCTS never
    // exceeds the 255 SIMD-0339 account ceiling in practice.
    info_first: [u16; MAX_ACCTS],
    info_writable: [bool; MAX_ACCTS],
    info_signer: [bool; MAX_ACCTS],
    info_count: usize,
    data: [MaybeUninit<u8>; MAX_DATA],
    data_len: usize,
}

impl<'a, const MAX_ACCTS: usize, const MAX_DATA: usize> DynCpi<'a, MAX_ACCTS, MAX_DATA> {
    /// Start a new dynamic CPI against the given program.
    #[inline]
    pub fn new(program_id: &'a Address) -> Self {
        Self {
            program_id,
            accounts: [const { MaybeUninit::uninit() }; MAX_ACCTS],
            writable: [false; MAX_ACCTS],
            signer: [false; MAX_ACCTS],
            account_count: 0,
            info_first: [0u16; MAX_ACCTS],
            info_writable: [false; MAX_ACCTS],
            info_signer: [false; MAX_ACCTS],
            info_count: 0,
            data: [const { MaybeUninit::uninit() }; MAX_DATA],
            data_len: 0,
        }
    }

    /// Append one account meta. The `writable` and `signer` flags
    /// are carried through to the emitted CPI instruction.
    ///
    /// Each call appends one *meta*. Order and duplicates are preserved because
    /// the callee reads accounts positionally. In parallel, the builder folds
    /// the account into a deduplicated *info* set keyed by pubkey: pushing
    /// an address already present does **not** allocate a second info slot,
    /// it reuses the existing one and OR-merges the writable/signer flags.
    /// Repeated metas for the same address therefore share one account-info at
    /// submit time. See [`Self::info_count`].
    ///
    /// Returns `Err(ProgramError::InvalidArgument)` when the builder
    /// is already at `MAX_ACCTS` capacity. Users pick the capacity
    /// at the type parameter; bumping it is a type-system edit, not
    /// a runtime error.
    #[inline]
    pub fn push_account(
        &mut self,
        account: &'a AccountView<'a>,
        writable: bool,
        signer: bool,
    ) -> ProgramResult {
        if self.account_count >= MAX_ACCTS {
            return Err(ProgramError::InvalidArgument);
        }
        let idx = self.account_count;
        self.accounts[idx] = MaybeUninit::new(account);
        self.writable[idx] = writable;
        self.signer[idx] = signer;

        // Fold into the deduped info projection (match by pubkey).
        let mut k = 0;
        let mut merged = false;
        while k < self.info_count {
            // SAFETY: `info_first[k] < account_count`, so that `accounts`
            // slot was initialized by an earlier `push_account`.
            let existing = unsafe { self.accounts[self.info_first[k] as usize].assume_init() };
            if address_eq(existing.address(), account.address()) {
                self.info_writable[k] |= writable;
                self.info_signer[k] |= signer;
                merged = true;
                break;
            }
            k += 1;
        }
        if !merged {
            let slot = self.info_count;
            self.info_first[slot] = idx as u16;
            self.info_writable[slot] = writable;
            self.info_signer[slot] = signer;
            self.info_count = self.info_count.wrapping_add(1);
        }

        self.account_count = self.account_count.wrapping_add(1);
        Ok(())
    }

    /// Append the given bytes to the instruction data buffer.
    ///
    /// Returns `Err(ProgramError::InvalidArgument)` when the buffer
    /// does not have room for the full slice. The append is
    /// all-or-nothing; a partial write does not happen.
    #[inline]
    pub fn push_data(&mut self, bytes: &[u8]) -> ProgramResult {
        if self.data_len.saturating_add(bytes.len()) > MAX_DATA {
            return Err(ProgramError::InvalidArgument);
        }
        let dst = &mut self.data[self.data_len..self.data_len + bytes.len()];
        for (i, b) in bytes.iter().enumerate() {
            dst[i] = MaybeUninit::new(*b);
        }
        self.data_len = self.data_len.wrapping_add(bytes.len());
        Ok(())
    }

    /// Append one byte. Sugar for programs that build instruction
    /// data one discriminator + one argument at a time.
    #[inline]
    pub fn push_byte(&mut self, byte: u8) -> ProgramResult {
        self.push_data(core::slice::from_ref(&byte))
    }

    /// Append the little-endian encoding of a `u64`. Covers the
    /// most common arg shape (lamports, timestamps, flags).
    #[inline]
    pub fn push_u64_le(&mut self, value: u64) -> ProgramResult {
        self.push_data(&value.to_le_bytes())
    }

    /// Append a 32-byte pubkey.
    #[inline]
    pub fn push_pubkey(&mut self, address: &Address) -> ProgramResult {
        self.push_data(address.as_array())
    }

    /// Current account (meta) count, one per `push_account`, including
    /// duplicates. This is the length of the ordered meta list the callee
    /// sees, *not* the deduped info count (see [`Self::info_count`]).
    #[inline(always)]
    pub const fn account_count(&self) -> usize {
        self.account_count
    }

    /// Number of *unique* account-infos after SIMD-0339 pubkey dedup.
    ///
    /// This is `<= account_count()`, and is exactly the count of
    /// account-infos handed to the syscall at submit time. Pushing the same
    /// address twice leaves this unchanged.
    #[inline(always)]
    pub const fn info_count(&self) -> usize {
        self.info_count
    }

    /// The `k`-th deduplicated account-info: its view plus the OR-merged
    /// `(writable, signer)` privilege across every occurrence. Returns
    /// `None` for `k >= info_count()`.
    ///
    /// Infos are in first-occurrence (push) order, so `dedup_info(0)` is the
    /// account whose first push came first.
    #[inline]
    pub fn dedup_info(&self, k: usize) -> Option<(&'a AccountView<'a>, bool, bool)> {
        if k >= self.info_count {
            return None;
        }
        // SAFETY: `k < info_count`, so `info_first[k] < account_count` names
        // an initialized `accounts` slot.
        let view = unsafe { self.accounts[self.info_first[k] as usize].assume_init() };
        Some((view, self.info_writable[k], self.info_signer[k]))
    }

    /// Program id this dynamic CPI targets.
    #[inline(always)]
    pub const fn program_id(&self) -> &Address {
        self.program_id
    }

    /// Current data length.
    #[inline(always)]
    pub const fn data_len(&self) -> usize {
        self.data_len
    }

    /// Borrow the finalized data buffer. Useful for tests that
    /// want to inspect the wire bytes without actually submitting
    /// the CPI.
    #[inline]
    pub fn data(&self) -> &[u8] {
        // SAFETY: This block is part of Hopper's reviewed zero-copy/backend boundary; surrounding checks and caller contracts uphold the required raw-pointer, layout, and aliasing invariants.
        unsafe { core::slice::from_raw_parts(self.data.as_ptr() as *const u8, self.data_len) }
    }

    /// The pushed account views, in push order.
    #[inline]
    pub fn account_views(&self) -> &[&'a AccountView<'a>] {
        // SAFETY: slots `0..account_count` were initialized by
        // `push_account`, and we expose exactly that prefix.
        unsafe {
            core::slice::from_raw_parts(
                self.accounts.as_ptr() as *const &'a AccountView<'a>,
                self.account_count,
            )
        }
    }

    /// Submit the built CPI (no PDA signers).
    ///
    /// Equivalent to [`invoke_signed`](Self::invoke_signed) with an
    /// empty signer set.
    #[inline]
    pub fn invoke(&self) -> ProgramResult {
        self.invoke_signed(&[])
    }

    /// Submit the built CPI with the given PDA signer seeds.
    ///
    /// Assembles the pushed `(account, writable, signer)` metas, preserving the
    /// full ordered list and duplicates, and the data buffer into an
    /// [`InstructionView`], then routes it through the **validated,
    /// dedup-aware** path
    /// ([`cpi::invoke_signed_deduped`](crate::cpi::invoke_signed_deduped)).
    /// The metas define what the callee sees positionally; the account-info
    /// list handed to the syscall is the pubkey-deduplicated set (one info
    /// per unique account, flags OR-merged). Address/flag agreement, PDA-signer
    /// resolution, live-borrow checks, and duplicate-writable rejection all
    /// run over the full meta list before the syscall, so deduplication does not
    /// skip those validation steps.
    #[inline]
    pub fn invoke_signed(&self, signers: &[Signer<'_, '_>]) -> ProgramResult {
        let count = self.account_count;
        let views = self.account_views();

        // Full ordered meta list, one meta per push, duplicates kept.
        let mut metas: [MaybeUninit<InstructionAccount<'a>>; MAX_ACCTS] =
            [const { MaybeUninit::uninit() }; MAX_ACCTS];
        let mut i = 0;
        while i < count {
            metas[i] = MaybeUninit::new(InstructionAccount::new(
                views[i].address(),
                self.writable[i],
                self.signer[i],
            ));
            i += 1;
        }
        // SAFETY: slots `0..count` were initialized by the loop above.
        let metas_slice = unsafe {
            core::slice::from_raw_parts(metas.as_ptr() as *const InstructionAccount<'a>, count)
        };
        let instruction = InstructionView {
            program_id: self.program_id,
            data: self.data(),
            accounts: metas_slice,
        };

        // Deduplicated account-info list, one AccountView per unique
        // address, in first-occurrence order.
        let mut infos: [MaybeUninit<&'a AccountView<'a>>; MAX_ACCTS] =
            [const { MaybeUninit::uninit() }; MAX_ACCTS];
        let mut k = 0;
        while k < self.info_count {
            // SAFETY: `info_first[k] < account_count` names an initialized
            // `accounts` slot.
            let view = unsafe { self.accounts[self.info_first[k] as usize].assume_init() };
            infos[k] = MaybeUninit::new(view);
            k += 1;
        }
        // SAFETY: slots `0..info_count` were initialized by the loop above.
        let infos_slice = unsafe {
            core::slice::from_raw_parts(
                infos.as_ptr() as *const &'a AccountView<'a>,
                self.info_count,
            )
        };

        crate::cpi::invoke_signed_deduped::<MAX_ACCTS>(&instruction, infos_slice, signers)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn byte_push_walks_the_buffer() {
        let program = Address::from([0u8; 32]);
        let mut cpi: DynCpi<4, 32> = DynCpi::new(&program);
        cpi.push_byte(0xA1).unwrap();
        cpi.push_u64_le(0xCAFEBABE_u64).unwrap();
        assert_eq!(cpi.data_len(), 1 + 8);
        assert_eq!(cpi.data()[0], 0xA1);
        assert_eq!(&cpi.data()[1..9], &0xCAFEBABE_u64.to_le_bytes());
    }

    #[test]
    fn data_overflow_rejects() {
        let program = Address::from([0u8; 32]);
        let mut cpi: DynCpi<0, 4> = DynCpi::new(&program);
        cpi.push_u64_le(1).expect_err("u64 is 8 bytes, buffer is 4");
    }

    #[test]
    fn push_pubkey_fills_32_bytes() {
        let program = Address::from([0u8; 32]);
        let mut cpi: DynCpi<0, 64> = DynCpi::new(&program);
        let pk = Address::from([0x7Au8; 32]);
        cpi.push_pubkey(&pk).unwrap();
        assert_eq!(cpi.data_len(), 32);
        assert!(cpi.data().iter().all(|b| *b == 0x7A));
    }

    mod invoke {
        use super::*;
        use hopper_native::{
            AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount,
            NOT_BORROWED,
        };

        fn make_account(
            address_byte: u8,
            is_signer: bool,
            is_writable: bool,
        ) -> (std::vec::Vec<u64>, AccountView<'static>) {
            let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + 8).div_ceil(8)];
            let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
            // SAFETY: backing is sized for the header plus data and
            // outlives the returned view.
            unsafe {
                raw.write(RuntimeAccount {
                    borrow_state: NOT_BORROWED,
                    is_signer: is_signer as u8,
                    is_writable: is_writable as u8,
                    executable: 0,
                    resize_delta: 0,
                    address: NativeAddress::new_from_array([address_byte; 32]),
                    owner: NativeAddress::new_from_array([2; 32]),
                    lamports: 1,
                    data_len: 8,
                });
            }
            // SAFETY: raw points at a fully initialized RuntimeAccount.
            let backend = unsafe { NativeAccountView::new_unchecked(raw) };
            (backing, AccountView::from_backend(backend))
        }

        #[test]
        fn invoke_validates_and_submits_built_metas() {
            let program = Address::from([9u8; 32]);
            let (_b1, signer_acct) = make_account(1, true, false);
            let (_b2, writable_acct) = make_account(2, false, true);

            let mut cpi: DynCpi<4, 16> = DynCpi::new(&program);
            cpi.push_account(&signer_acct, false, true).unwrap();
            cpi.push_account(&writable_acct, true, false).unwrap();
            cpi.push_byte(3).unwrap();
            cpi.push_u64_le(42).unwrap();

            // Off-chain the syscall is a no-op, but the full validation
            // pipeline (address/flag agreement, borrow checks,
            // duplicate-writable) runs against the metas this builder
            // assembled, proving the build→submit chain is wired.
            assert_eq!(cpi.invoke(), Ok(()));
        }

        #[test]
        fn invoke_surfaces_flag_mismatch_from_built_metas() {
            let program = Address::from([9u8; 32]);
            // The view is NOT a transaction signer, but the builder
            // declares it must sign (and no PDA seeds are provided):
            // the validated path must refuse.
            let (_b1, not_signer) = make_account(3, false, false);

            let mut cpi: DynCpi<2, 4> = DynCpi::new(&program);
            cpi.push_account(&not_signer, false, true).unwrap();
            assert_eq!(cpi.invoke(), Err(ProgramError::MissingRequiredSignature));
        }

        #[test]
        fn invoke_rejects_duplicate_writable_accounts() {
            let program = Address::from([9u8; 32]);
            let (_b1, first) = make_account(4, false, true);
            let (_b2, second) = make_account(4, false, true);

            let mut cpi: DynCpi<2, 4> = DynCpi::new(&program);
            cpi.push_account(&first, true, false).unwrap();
            cpi.push_account(&second, true, false).unwrap();
            assert_eq!(cpi.invoke(), Err(ProgramError::AccountBorrowFailed));
        }

        #[test]
        fn account_views_exposes_push_order_prefix() {
            let program = Address::from([9u8; 32]);
            let (_b1, a) = make_account(5, false, false);
            let (_b2, b) = make_account(6, false, false);

            let mut cpi: DynCpi<4, 4> = DynCpi::new(&program);
            cpi.push_account(&a, false, false).unwrap();
            cpi.push_account(&b, false, false).unwrap();
            let views = cpi.account_views();
            assert_eq!(views.len(), 2);
            assert_eq!(views[0].address(), &Address::from([5u8; 32]));
            assert_eq!(views[1].address(), &Address::from([6u8; 32]));
        }

        // -- SIMD-0339 account-info dedup --------------------------------

        #[test]
        fn repeated_address_collapses_to_one_info_with_or_merged_flags() {
            let program = Address::from([9u8; 32]);
            // One underlying account, pushed twice with complementary flags.
            let (_b, acct) = make_account(7, true, true);

            let mut cpi: DynCpi<4, 4> = DynCpi::new(&program);
            cpi.push_account(&acct, true, false).unwrap(); // writable, not signer
            cpi.push_account(&acct, false, true).unwrap(); // not writable, signer

            // Both pushes are kept as metas...
            assert_eq!(cpi.account_count(), 2);
            // ...but collapse to a single deduplicated account-info.
            assert_eq!(cpi.info_count(), 1);

            let (view, writable, signer) = cpi.dedup_info(0).unwrap();
            assert_eq!(view.address(), &Address::from([7u8; 32]));
            // Flags are the OR across occurrences: writable in push #1,
            // signer in push #2 => the single info is both.
            assert!(writable, "writable in any meta => info writable");
            assert!(signer, "signer in any meta => info signer");
            assert!(cpi.dedup_info(1).is_none());
        }

        #[test]
        fn distinct_addresses_stay_distinct_infos() {
            let program = Address::from([9u8; 32]);
            let (_b1, a) = make_account(5, false, false);
            let (_b2, b) = make_account(6, false, false);

            let mut cpi: DynCpi<4, 4> = DynCpi::new(&program);
            cpi.push_account(&a, false, false).unwrap();
            cpi.push_account(&b, false, false).unwrap();

            assert_eq!(cpi.account_count(), 2);
            assert_eq!(cpi.info_count(), 2);
            assert_eq!(
                cpi.dedup_info(0).unwrap().0.address(),
                &Address::from([5u8; 32])
            );
            assert_eq!(
                cpi.dedup_info(1).unwrap().0.address(),
                &Address::from([6u8; 32])
            );
        }

        #[test]
        fn metas_preserve_order_and_duplicates_while_infos_dedup() {
            let program = Address::from([9u8; 32]);
            let (_b1, a) = make_account(5, false, false);
            let (_b2, b) = make_account(6, false, false);

            // Push order a, b, a: the middle account is distinct, the third
            // repeats the first.
            let mut cpi: DynCpi<4, 4> = DynCpi::new(&program);
            cpi.push_account(&a, false, false).unwrap();
            cpi.push_account(&b, false, false).unwrap();
            cpi.push_account(&a, false, false).unwrap();

            // Metas: all three, in push order, duplicate preserved.
            let metas = cpi.account_views();
            assert_eq!(metas.len(), 3);
            assert_eq!(metas[0].address(), &Address::from([5u8; 32]));
            assert_eq!(metas[1].address(), &Address::from([6u8; 32]));
            assert_eq!(metas[2].address(), &Address::from([5u8; 32]));

            // Infos: two unique, in first-occurrence order.
            assert_eq!(cpi.info_count(), 2);
            assert_eq!(
                cpi.dedup_info(0).unwrap().0.address(),
                &Address::from([5u8; 32])
            );
            assert_eq!(
                cpi.dedup_info(1).unwrap().0.address(),
                &Address::from([6u8; 32])
            );
        }

        #[test]
        fn invoke_submits_deduped_repeated_readonly_account() {
            let program = Address::from([9u8; 32]);
            let (_b, acct) = make_account(8, false, false);

            let mut cpi: DynCpi<4, 4> = DynCpi::new(&program);
            cpi.push_account(&acct, false, false).unwrap();
            cpi.push_account(&acct, false, false).unwrap();
            cpi.push_account(&acct, false, false).unwrap();

            // Three read-only metas of one account collapse to one info.
            assert_eq!(cpi.account_count(), 3);
            assert_eq!(cpi.info_count(), 1);
            // Off-chain the syscall is a no-op; Ok proves the dedup-aware
            // validation pipeline accepted the built instruction.
            assert_eq!(cpi.invoke(), Ok(()));
        }

        #[test]
        fn wide_dyn_cpi_exceeds_legacy_64_account_ceiling() {
            let program = Address::from([9u8; 32]);
            // 65 distinct accounts, one past the pre-SIMD-0339 static
            // ceiling of 64. Keep backings and views alive for the builder.
            let mut backings: std::vec::Vec<std::vec::Vec<u64>> = std::vec::Vec::new();
            let mut views: std::vec::Vec<AccountView<'static>> = std::vec::Vec::new();
            for i in 1..=65u8 {
                let (b, v) = make_account(i, false, false);
                backings.push(b);
                views.push(v);
            }

            let mut cpi: DynCpi<70, 4> = DynCpi::new(&program);
            for v in &views {
                cpi.push_account(v, false, false).unwrap();
            }

            assert_eq!(cpi.account_count(), 65);
            // All distinct, so no dedup shrinkage here; but the shape is
            // accepted, proving >64 account CPIs build and submit.
            assert_eq!(cpi.info_count(), 65);
            assert_eq!(cpi.invoke(), Ok(()));
        }
    }
}