diff --git a/bench-report.json b/bench-report.json index 2b9053c..43acb7b 100644 --- a/bench-report.json +++ b/bench-report.json @@ -34,15 +34,15 @@ "reclaim_buffer/max_buffers_in_one_instruction": 136501, "reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18043, "reclaim_order/happy_path_returns_lamports_and_closes_pda": 2133, - "settle/finalizes_with_no_pushes": 7043, - "settle/pulls_from_multiple_orders": 19750, - "settle/pulls_funds_to_destination": 13417, - "settle/pulls_to_multiple_destinations": 14564, - "settle/pushes_a_single_order": 12267, - "settle/pushes_several_orders_from_different_buffers": 17451, - "settle/pushes_several_orders_from_one_buffer": 17452, - "settle/settles_a_single_order": 12285, - "settle/settles_multiple_orders": 22679, + "settle/finalizes_with_no_pushes": 7264, + "settle/pulls_from_multiple_orders": 20345, + "settle/pulls_funds_to_destination": 13922, + "settle/pulls_to_multiple_destinations": 15065, + "settle/pushes_a_single_order": 12776, + "settle/pushes_several_orders_from_different_buffers": 18054, + "settle/pushes_several_orders_from_one_buffer": 18055, + "settle/settles_a_single_order": 12794, + "settle/settles_multiple_orders": 23549, "transfer_authority/manager_can_transfer_manager": 3170, "transfer_authority/manager_can_transfer_reclaim_authority": 3172, "transfer_authority/reclaim_authority_can_transfer_itself": 3175 diff --git a/interface/src/instruction/settle/begin.rs b/interface/src/instruction/settle/begin.rs index 4a9f525..9139003 100644 --- a/interface/src/instruction/settle/begin.rs +++ b/interface/src/instruction/settle/begin.rs @@ -1,7 +1,5 @@ //! Off-chain builder and input parsing for the `BeginSettle` instruction. -use std::vec; - use solana_instruction::{AccountMeta, Instruction}; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; @@ -11,6 +9,25 @@ use crate::{SettlementError, SettlementInstruction}; use super::{recover_counterpart, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID}; +/// The number of fixed accounts every `BeginSettle` carries before its per-order +/// accounts: the instructions sysvar, the settlement state PDA, and the token +/// program. +const BEGIN_FIXED_ACCOUNTS: usize = 3; + +/// The fixed accounts every `BeginSettle` carries, in wire order, ahead of its +/// per-order `[order_pda, sell_token_account, destinations..]` groups. +/// +/// The return type ties the list to [`BEGIN_FIXED_ACCOUNTS`]: adding or removing +/// a fixed account here without updating that count is a compile error, and +/// `parse_body`'s leading slice pattern must destructure exactly this many names. +fn fixed_accounts(state_pda: Pubkey) -> [AccountMeta; BEGIN_FIXED_ACCOUNTS] { + [ + AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), + AccountMeta::new_readonly(state_pda, false), + AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), + ] +} + /// A single transfer made when settling an order: `amount` tokens sent from the /// order's sell token account to `destination`. #[derive(Clone, Debug, Eq, PartialEq)] @@ -90,11 +107,7 @@ impl From> for Instruction { // Read-only accounts for instruction introspection, settlement state, and // the SPL token program. - let mut accounts = vec![ - AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), - AccountMeta::new_readonly(state_pda, false), - AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), - ]; + let mut accounts = Vec::from(fixed_accounts(state_pda)); for &i in &order { // Writable account for the order: `BeginSettle` updates its filled // amounts (`amount_withdrawn`/`amount_received`). @@ -287,7 +300,7 @@ mod tests { /// The fixed accounts every `BeginSettle` carries before its order accounts: /// the instructions sysvar, the settlement state PDA, and the token program. - const FIXED_ACCOUNTS: usize = 3; + use super::BEGIN_FIXED_ACCOUNTS as FIXED_ACCOUNTS; /// A placeholder auction id for the tests where its specific value is /// incidental. The wire-layout tests spell out the literal bytes instead. diff --git a/interface/src/instruction/settle/finalize.rs b/interface/src/instruction/settle/finalize.rs index 5ef15d8..b763473 100644 --- a/interface/src/instruction/settle/finalize.rs +++ b/interface/src/instruction/settle/finalize.rs @@ -1,20 +1,38 @@ //! Off-chain builder and input parsing for the `FinalizeSettle` instruction. -use std::vec; - use solana_instruction::{AccountMeta, Instruction}; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; use crate::instruction::InstructionInputParsing; -use crate::{recover_discriminator, SettlementError, SettlementInstruction}; +use crate::{SettlementError, SettlementInstruction}; use super::{recover_counterpart, INSTRUCTIONS_SYSVAR_ID, SPL_TOKEN_PROGRAM_ID}; /// The number of fixed accounts every `FinalizeSettle` carries before its push /// accounts: the instructions sysvar, the settlement state PDA, and the token /// program. -pub const FINALIZE_FIXED_ACCOUNTS: usize = 3; +/// +/// Crate-private on purpose: the layout it describes is consumed only by the +/// builder and the parser below. Anything that needs to read a `FinalizeSettle` +/// — the settlement program's `BeginSettle` included — goes through +/// [`FinalizeSettleInput`] rather than re-deriving offsets from this count. +const FINALIZE_FIXED_ACCOUNTS: usize = 3; + +/// The fixed accounts every `FinalizeSettle` carries, in wire order, ahead of +/// its `[source_buffer, destination]` push pairs. +/// +/// The return type ties the list to [`FINALIZE_FIXED_ACCOUNTS`]: adding or +/// removing a fixed account here without updating that count is a compile +/// error, and the `parser_recovers_builder_pushes` property test pins the +/// parser's leading slice pattern to the same count. +fn fixed_accounts(state_pda: Pubkey) -> [AccountMeta; FINALIZE_FIXED_ACCOUNTS] { + [ + AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), + AccountMeta::new_readonly(state_pda, false), + AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), + ] +} /// Split the instruction bytes from `FinalizeSettle` that remain after all /// constant-size data has been extracted into the per-push bump bytes and the @@ -35,30 +53,6 @@ fn split_push_bytes(body: &[u8]) -> Result<(&[u8], &[[u8; 8]]), ProgramError> { Ok((bumps, amounts)) } -/// Like [`split_push_bytes`], but yields the amounts already decoded as `u64`s -/// so callers streaming the amounts don't each re-decode the little-endian -/// bytes. The decoding is lazy, so nothing is allocated. -fn split_pushes(body: &[u8]) -> Result<(&[u8], impl Iterator + '_), ProgramError> { - let (bumps, amounts) = split_push_bytes(body)?; - Ok((bumps, amounts.iter().copied().map(u64::from_le_bytes))) -} - -/// The push amounts, in push order, carried by a `FinalizeSettle` instruction, -/// recovered from its full instruction data alone (discriminator included, as -/// returned by instruction introspection). -/// -/// This function doesn't otherwise check that the instruction is consistent -/// with a `FinalizeSettle` instruction. For example, the discriminator field is -/// ignored. -pub fn finalize_push_amounts( - instruction_data: &[u8], -) -> Result + '_, ProgramError> { - let (_discriminator, rest) = recover_discriminator(instruction_data)?; - let (_begin_ix_index, body) = recover_counterpart(rest)?; - let (_bumps, amounts) = split_pushes(body)?; - Ok(amounts) -} - /// Builder for a `FinalizeSettle` instruction pushing the funds described by the /// parallel lists: /// - `source_buffers[i]` is the buffer token account the funds come from, @@ -116,11 +110,7 @@ impl From> for Instruction { .chain(amounts.iter().flat_map(|amount| amount.to_le_bytes())) .collect(); - let mut accounts = vec![ - AccountMeta::new_readonly(INSTRUCTIONS_SYSVAR_ID, false), - AccountMeta::new_readonly(state_pda, false), - AccountMeta::new_readonly(SPL_TOKEN_PROGRAM_ID, false), - ]; + let mut accounts = Vec::from(fixed_accounts(state_pda)); for (source, destination) in source_buffers.iter().zip(destinations) { accounts.push(AccountMeta::new(*source, false)); accounts.push(AccountMeta::new(*destination, false)); @@ -573,25 +563,7 @@ mod tests { } #[test] - fn finalize_push_amounts_extracts_amounts() { - let amounts = [0x0102, 0x0304]; - let ix = Instruction::from(FinalizeSettle { - program_id: Pubkey::new_unique(), - state_pda: Pubkey::new_unique(), - begin_ix_index: 0x1337, - source_buffers: &[Pubkey::new_unique(), Pubkey::new_unique()], - destinations: &[Pubkey::new_unique(), Pubkey::new_unique()], - bumps: &[0xa1, 0xb1], - amounts: &amounts, - }); - - let recovered = finalize_push_amounts(&ix.data).expect("valid finalize data"); - let decoded: Vec = recovered.collect(); - assert_eq!(decoded, amounts); - } - - #[test] - fn finalize_push_amounts_handles_no_pushes() { + fn finalize_settle_input_handles_no_pushes() { let ix = Instruction::from(FinalizeSettle { program_id: Pubkey::new_unique(), state_pda: Pubkey::new_unique(), @@ -601,79 +573,71 @@ mod tests { bumps: &[], amounts: &[], }); - let mut amounts = finalize_push_amounts(&ix.data).expect("valid empty finalize data"); - assert!(amounts.next().is_none()); - } - - #[test] - fn finalize_push_amounts_rejects_incorrect_bytes() { - let mut ix = Instruction::from(FinalizeSettle { - program_id: Pubkey::new_unique(), - state_pda: Pubkey::new_unique(), - begin_ix_index: 0, - source_buffers: &[Pubkey::new_unique()], - destinations: &[Pubkey::new_unique()], - bumps: &[0xff], - amounts: &[31337], - }); - ix.data.pop(); - assert_eq!( - finalize_push_amounts(&ix.data).err(), - Some(ProgramError::InvalidInstructionData), - ); - } - - /// An arbitrary well-formed `FinalizeSettle` instruction with `push_count` - /// pushes. - fn arb_finalize_instruction( - push_count: impl Into, - ) -> impl Strategy { - ( - any::<[u8; 32]>().prop_map(Pubkey::new_from_array), - any::<[u8; 32]>().prop_map(Pubkey::new_from_array), - any::(), - crate::instruction::settle::fixtures::arb_pushes(push_count), - ) - .prop_map( - |( - program_id, - state_pda, - begin_ix_index, - (source_buffers, destinations, bumps, amounts), - )| { - Instruction::from(FinalizeSettle { - program_id, - state_pda, - begin_ix_index, - source_buffers: &source_buffers, - destinations: &destinations, - bumps: &bumps, - amounts: &amounts, - }) - }, - ) + let accounts = fake_sequential_accounts::(); + let parsed = FinalizeSettleInput::parse(&ix.data, &accounts) + .expect("an empty finalize still parses"); + assert!(parsed.pushes.iter().next().is_none()); } proptest! { - /// For any well-formed `FinalizeSettle`, the amounts `finalize_push_amounts` - /// recovers from the instruction data alone (the way `BeginSettle` sees it - /// through introspection) must equal the amounts the full - /// `FinalizeSettleInput` parser reads from the same data plus its accounts. + /// The builder and the parser must agree on the wire format: for any + /// well-formed `FinalizeSettle`, parsing it back recovers every push + /// the builder was handed, in order. + /// + /// This is what pins the two halves of the layout together, so it covers + /// all four push fields rather than just the amounts: the builder writes + /// the source buffer and destination as account metas but the bump and + /// amount into the instruction data, and a drift in either path has to + /// fail here. #[test] - fn finalize_push_amounts_matches_parser(ix in arb_finalize_instruction(0..=16usize)) { - // Recovered from the instruction data alone. - let recovered: Vec = - finalize_push_amounts(&ix.data).expect("well-formed finalize data").collect(); + fn parser_recovers_builder_pushes( + program_id in any::<[u8; 32]>().prop_map(Pubkey::new_from_array), + state_pda in any::<[u8; 32]>().prop_map(Pubkey::new_from_array), + begin_ix_index in any::(), + (source_buffers, destinations, bumps, amounts) + in crate::instruction::settle::fixtures::arb_pushes(0..=16usize), + ) { + let ix = Instruction::from(FinalizeSettle { + program_id, + state_pda, + begin_ix_index, + source_buffers: &source_buffers, + destinations: &destinations, + bumps: &bumps, + amounts: &amounts, + }); - // Read by the full parser from the same data plus its accounts. let accounts: Vec = ix.accounts.iter().map(|meta| fake_account(meta.pubkey)).collect(); let parsed = FinalizeSettleInput::parse(&ix.data, &accounts) .expect("a well-formed finalize parses"); - let parsed_amounts: Vec = - parsed.pushes.iter().map(|push| push.amount).collect(); - prop_assert_eq!(recovered, parsed_amounts); + let recovered: Vec<(Address, Address, u8, u64)> = parsed + .pushes + .iter() + .map(|push| ( + *push.source_buffer.address(), + *push.destination.address(), + push.bump, + push.amount, + )) + .collect(); + + // The builder's own inputs, the ground truth the parse must reproduce. + let expected: Vec<(Address, Address, u8, u64)> = source_buffers + .iter() + .zip(&destinations) + .zip(&bumps) + .zip(&amounts) + .map(|(((source, destination), bump), amount)| ( + Address::new_from_array(source.to_bytes()), + Address::new_from_array(destination.to_bytes()), + *bump, + *amount, + )) + .collect(); + + prop_assert_eq!(&recovered, &expected); } } } diff --git a/interface/src/instruction/settle/mod.rs b/interface/src/instruction/settle/mod.rs index 85ce25e..e5d14b5 100644 --- a/interface/src/instruction/settle/mod.rs +++ b/interface/src/instruction/settle/mod.rs @@ -10,10 +10,7 @@ mod begin; mod finalize; pub use begin::{BeginSettle, BeginSettleInput, Pull, SettledOrder, SettledOrders}; -pub use finalize::{ - finalize_push_amounts, FinalizeSettle, FinalizeSettleInput, Push, Pushes, - FINALIZE_FIXED_ACCOUNTS, -}; +pub use finalize::{FinalizeSettle, FinalizeSettleInput, Push, Pushes}; /// Reads the first two bytes of a byte slice (instruction data) and /// interprets them as a little-endian u16, returning it together with the diff --git a/programs/settlement/src/settle/begin.rs b/programs/settlement/src/settle/begin.rs index e9aad48..32febde 100644 --- a/programs/settlement/src/settle/begin.rs +++ b/programs/settlement/src/settle/begin.rs @@ -8,10 +8,7 @@ use cow_settlement_interface::{ order::{EncodedOrderAccount, OrderAccount}, }, instruction::{ - settle::{ - finalize_push_amounts, BeginSettleInput, SettledOrder, SettledOrders, - FINALIZE_FIXED_ACCOUNTS, - }, + settle::{BeginSettleInput, FinalizeSettleInput, SettledOrder, SettledOrders}, InstructionInputParsing, }, recover_discriminator, SettlementError, SettlementInstruction, @@ -69,6 +66,15 @@ pub fn process_begin_settle( let finalize_ix = instructions.load_instruction_at(usize::from(input.finalize_ix_index))?; + // Read the paired `FinalizeSettle` through the same parser that instruction + // runs on itself, so its account layout is spelled out in exactly one place. + // `parse` re-checks the discriminator and rejects a push count that + // disagrees with the accounts, which `validate_counterpart` above doesn't + // look at. + let finalize_accounts = introspected_accounts(&finalize_ix)?; + let finalize = + FinalizeSettleInput::parse(finalize_ix.get_instruction_data(), &finalize_accounts)?; + validate_token_program_account(input.token_program_account)?; with_state_pda_signer(program_id, input.state_pda_account, |state_pda_signer| { @@ -77,45 +83,28 @@ pub fn process_begin_settle( input.state_pda_account, state_pda_signer, &input.orders, - &finalize_ix, + &finalize, ) }) } -/// The destination address of each push carried by the paired `FinalizeSettle`, -/// seen through instruction introspection, in order. +/// The addresses of an introspected instruction's accounts, in order. /// -/// The push structure isn't validated here: the paired `FinalizeSettle` re-parses -/// the same instruction from its own data and rejects a dangling source buffer or -/// a push count that disagrees with its accounts. The caller pairs these -/// destinations with the settled orders one-to-one, which is what catches a count -/// mismatch. -fn push_destinations<'a>( - instruction: &'a IntrospectedInstruction<'a>, -) -> impl Iterator { - // Each push occupies a `[source_buffer, destination]` meta pair after the - // fixed accounts, so the destinations are every second meta beginning at the - // first push's destination. - (FINALIZE_FIXED_ACCOUNTS + 1..instruction.num_account_metas()) - .step_by(2) - .map(|destination_index| { - // The index stays below `num_account_metas`, so the lookup, whose only - // error is an out-of-bounds index, always succeeds. - &instruction - .get_instruction_account_at(destination_index) - .expect("index within num_account_metas") - .key +/// Instruction introspection hands out account metas one index at a time, while +/// the shared instruction parsers borrow a slice. Collecting the addresses +/// bridges the two so `BeginSettle` can read its counterpart with the parser +/// that counterpart uses on itself, instead of walking the metas at hardcoded +/// offsets and duplicating the account layout. +fn introspected_accounts( + instruction: &IntrospectedInstruction<'_>, +) -> Result, ProgramError> { + (0..instruction.num_account_metas()) + .map(|index| { + instruction + .get_instruction_account_at(index) + .map(|meta| meta.key) }) -} - -/// The paired pushes `BeginSettle` settles against: each push's destination -/// (read from the finalize's account metas) with the amount it pays in (read -/// from the finalize's instruction data), in push order. -fn finalize_pushes<'a>( - finalize_ix: &'a IntrospectedInstruction<'a>, -) -> Result, ProgramError> { - let amounts = finalize_push_amounts(finalize_ix.get_instruction_data())?; - Ok(push_destinations(finalize_ix).zip(amounts)) + .collect() } /// Reject a `BeginSettle` whose pair encloses another settlement: no @@ -178,7 +167,7 @@ fn settle_orders( state_pda_account: &AccountView, state_pda_signer: &Signer, orders: &SettledOrders<'_, AccountView>, - finalize_ix: &IntrospectedInstruction, + finalize: &FinalizeSettleInput<'_, Address>, ) -> ProgramResult { // Orders must be passed strictly increasing by address; this rejects // duplicates (settling the same order twice) without a separate scan. @@ -189,7 +178,10 @@ fn settle_orders( // Pull one push (destination and amount) per order; running out mid-loop // means fewer pushes than orders. A leftover push (more pushes than orders) // is caught after. - let mut pushes = finalize_pushes(finalize_ix)?; + let mut pushes = finalize + .pushes + .iter() + .map(|push| (push.destination, push.amount)); for order in orders.iter() { let order_pda_address = *order.order_pda.address(); @@ -962,13 +954,15 @@ mod tests { } proptest! { - /// `BeginSettle` settles against a paired `FinalizeSettle`'s pushes via - /// `finalize_pushes`: each destination (from the account metas) paired - /// with its amount (from the instruction data). For any well-formed - /// finalize those pairs must match both the builder's inputs and what - /// `FinalizeSettleInput` parses from the same instruction. + /// `BeginSettle` reads its paired `FinalizeSettle` through instruction + /// introspection, feeding the introspected account addresses to the same + /// `FinalizeSettleInput` parser the finalize runs on its own + /// `AccountView`s. Both routes into that parser must recover the + /// builder's pushes identically: the introspected metas and the runtime + /// accounts are different account types reaching the same layout, and + /// nothing else pins the two together. #[test] - fn finalize_pushes_matches_parser( + fn introspected_parse_matches_account_parse( program_id in any::<[u8; 32]>(), state_pda in any::<[u8; 32]>(), begin_ix_index in any::(), @@ -985,9 +979,18 @@ mod tests { }); let introspected_instruction = introspected_instruction(&ix); - let introspected: Vec<(Address, u64)> = finalize_pushes(&introspected_instruction) + // The production route: introspected metas collected into a slice, + // then handed to the shared parser. + let introspected_accounts = introspected_accounts(&introspected_instruction) + .expect("indices stay within the meta count"); + let introspected: Vec<(Address, u64)> = FinalizeSettleInput::parse( + introspected_instruction.get_instruction_data(), + &introspected_accounts, + ) .expect("well-formed finalize data") - .map(|(&destination, amount)| (destination, amount)) + .pushes + .iter() + .map(|push| (*push.destination, push.amount)) .collect(); let accounts: Vec = diff --git a/programs/settlement/tests/finalize_settle_pushes.rs b/programs/settlement/tests/finalize_settle_pushes.rs index c367054..456b1df 100644 --- a/programs/settlement/tests/finalize_settle_pushes.rs +++ b/programs/settlement/tests/finalize_settle_pushes.rs @@ -38,6 +38,19 @@ fn assert_finalize_error(result: Result, expected: Instr ); } +/// Assert the transaction failed in `BeginSettle` (at [`BEGIN_INDEX`]) with +/// `expected`. +/// +/// `BeginSettle` reads its paired finalize through the same parser the finalize +/// applies to itself, so a finalize whose accounts and data disagree is rejected +/// here first — before any funds are pulled — rather than at the finalize. +fn assert_begin_error(result: Result, expected: InstructionError) { + assert_eq!( + result.err(), + Some(TransactionError::InstructionError(BEGIN_INDEX, expected)), + ); +} + /// Build the minimal `[BeginSettle, FinalizeSettle]` instructions that settle /// `orders` (begin) and push their proceeds (finalize). fn finalize(program_id: &Pubkey, orders: &[FinalizedIntent]) -> Vec { @@ -291,15 +304,14 @@ fn rejects_push_account_count_mismatch() { orders: &orders, }); // ...with another push's worth of data bytes appended but no matching - // accounts. `BeginSettle` derives the push count from the (unchanged) account - // metas (one push, matching its one order and paying the right destination) - // so it passes. Only the finalize reads the data, where it now parses two - // pushes against two push accounts and rejects the mismatch. This is the - // account/data disagreement `BeginSettle` structurally can't see. + // accounts. Both instructions parse the finalize the same way, so the + // account/data disagreement is caught by whichever runs first: `BeginSettle` + // reads two pushes out of the data, finds only one push's accounts, and + // rejects before pulling anything. finalize.data.extend_from_slice(&[0u8; 9]); let instructions = build_settlement(&program_id, &orders, finalize); - assert_finalize_error( + assert_begin_error( send(&mut svm, &payer, instructions), to_instruction_error(SettlementError::AccountCountNotMatchingPushCount), ); @@ -315,17 +327,16 @@ fn rejects_too_few_accounts() { begin_ix_index: BEGIN_INDEX.into(), orders: &[], }); - // ...with one of its three fixed accounts popped. `BeginSettle` runs first - // but only reads push destinations off the accounts (finding none, matching - // its zero orders) so it passes. The finalize then can't even destructure - // its fixed accounts and raises `NotEnoughAccountKeys`. + // ...with one of its three fixed accounts popped, so the finalize can't even + // destructure its fixed accounts. `BeginSettle` runs the same parser over the + // introspected metas, so it raises `NotEnoughAccountKeys` first. finalize.accounts.pop(); let instructions = build_settlement(&program_id, &[], finalize); let err = send(&mut svm, &payer, instructions) .expect_err("a finalize missing a fixed account must be rejected"); - let TransactionError::InstructionError(FINALIZE_INDEX, ix_err) = err else { - panic!("expected the finalize (index {FINALIZE_INDEX}) to fail, got {err:?}"); + let TransactionError::InstructionError(BEGIN_INDEX, ix_err) = err else { + panic!("expected the begin (index {BEGIN_INDEX}) to fail, got {err:?}"); }; // Compare against the non-deprecated `ProgramError` variant the program // returns; naming the `InstructionError` variant directly would touch a @@ -362,10 +373,10 @@ fn rejects_two_too_few_accounts() { finalize.accounts.pop(); // The paired `Begin` settles no orders, so it never checks the push - // destinations: the inconsistency is left for the finalize's own - // account-count check to reject. + // destinations — but it still parses the finalize in full, so it catches the + // declared push having no accounts behind it. let instructions = build_settlement(&program_id, &[], finalize); - assert_finalize_error( + assert_begin_error( send(&mut svm, &payer, instructions), to_instruction_error(SettlementError::AccountCountNotMatchingPushCount), );