Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions bench-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"reclaim_buffer/max_buffers_in_one_instruction": 64,
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 9,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 4,
"remove_solver/remove_with_many_existing_solvers": 5,
"remove_solver/removes_a_solver": 5,
"settle/finalizes_with_no_pushes": 5,
"settle/pulls_from_multiple_orders": 15,
"settle/pulls_funds_to_destination": 10,
Expand All @@ -28,16 +30,18 @@
"compute_units": {
"add_solver/add_with_many_existing_solvers": 5074,
"add_solver/adds_a_solver": 4622,
"create_buffers/happy_path_creates_initialized_buffer_token_account": 10345,
"create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21743,
"create_buffers/max_buffers_in_one_instruction": 177040,
"create_buffers/happy_path_creates_initialized_buffer_token_account": 10347,
"create_buffers/happy_path_creates_multiple_buffers_in_one_instruction": 21747,
"create_buffers/max_buffers_in_one_instruction": 177071,
"create_order/happy_path_creates_order_pda_with_expected_body": 4978,
"initialize/happy_path_initializes_state_pda_with_expected_data": 4529,
"initialize/happy_path_initializes_state_pda_with_expected_data": 4530,
"reclaim_buffer/funded_buffer_is_skipped": 6335,
"reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7483,
"reclaim_buffer/max_buffers_in_one_instruction": 136652,
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18082,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 2183,
"reclaim_buffer/happy_path_reclaims_empty_buffer_to_the_authority_itself": 7482,
"reclaim_buffer/max_buffers_in_one_instruction": 136622,
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 18081,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 2182,
"remove_solver/remove_with_many_existing_solvers": 3763,
"remove_solver/removes_a_solver": 3498,
"settle/finalizes_with_no_pushes": 7154,
"settle/pulls_from_multiple_orders": 20043,
"settle/pulls_funds_to_destination": 13632,
Expand All @@ -47,9 +51,9 @@
"settle/pushes_several_orders_from_one_buffer": 17750,
"settle/settles_a_single_order": 12505,
"settle/settles_multiple_orders": 23062,
"transfer_authority/manager_can_transfer_manager": 3174,
"transfer_authority/manager_can_transfer_reclaim_authority": 3176,
"transfer_authority/reclaim_authority_can_transfer_itself": 3180
"transfer_authority/manager_can_transfer_manager": 3173,
"transfer_authority/manager_can_transfer_reclaim_authority": 3175,
"transfer_authority/reclaim_authority_can_transfer_itself": 3179
},
"transaction_bytes": {
"add_solver/add_with_many_existing_solvers": 366,
Expand All @@ -64,6 +68,8 @@
"reclaim_buffer/max_buffers_in_one_instruction": 332,
"reclaim_buffer/reclaims_multiple_buffers_skipping_funded": 466,
"reclaim_order/happy_path_returns_lamports_and_closes_pda": 236,
"remove_solver/remove_with_many_existing_solvers": 365,
"remove_solver/removes_a_solver": 365,
"settle/finalizes_with_no_pushes": 290,
"settle/pulls_from_multiple_orders": 656,
"settle/pulls_funds_to_destination": 473,
Expand Down
23 changes: 23 additions & 0 deletions client/src/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,29 @@ impl From<AddSolver> for Instruction {
}
}

/// Removes `solver` from the state PDA's solver list. Authorized by `manager`;
/// the freed rent is paid to `rent_recipient`.
pub struct RemoveSolver {
pub program_id: Pubkey,
pub manager: Pubkey,
pub rent_recipient: Pubkey,
pub solver: Pubkey,
}

impl From<RemoveSolver> for Instruction {
fn from(builder: RemoveSolver) -> Self {
let (state_pda, _bump) = find_state_pda(&builder.program_id);
cow_settlement_interface::instruction::remove_solver::RemoveSolver {
program_id: builder.program_id,
manager: builder.manager,
rent_recipient: builder.rent_recipient,
state_pda,
solver: builder.solver,
}
.into()
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
16 changes: 15 additions & 1 deletion client/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use cow_settlement_interface::{
initialize::InitializeInput,
reclaim_buffer::ReclaimBufferInput,
reclaim_order::ReclaimOrderInput,
remove_solver::RemoveSolverInput,
settle::{BeginSettleInput, FinalizeSettleInput},
transfer_authority::TransferAuthorityInput,
InstructionInputParsing,
Expand All @@ -30,6 +31,7 @@ pub enum ParsedInstruction<'a, A> {
ReclaimBuffer(ReclaimBufferInput<'a, A>),
TransferAuthority(TransferAuthorityInput<'a, A>),
AddSolver(AddSolverInput<'a, A>),
RemoveSolver(RemoveSolverInput<'a, A>),
}

/// Parses any settlement instruction by its discriminator.
Expand Down Expand Up @@ -66,6 +68,9 @@ pub fn parse_instruction<'a, A>(
SettlementInstruction::AddSolver => {
ParsedInstruction::AddSolver(AddSolverInput::parse_body(remaining_data, accounts)?)
}
SettlementInstruction::RemoveSolver => ParsedInstruction::RemoveSolver(
RemoveSolverInput::parse_body(remaining_data, accounts)?,
),
})
}

Expand All @@ -74,7 +79,7 @@ mod tests {
use super::*;
use crate::instructions::{
AddSolver, BeginSettle, CreateBuffers, CreateOrder, FinalizeSettle, Initialize,
InitializedIntent,
InitializedIntent, RemoveSolver,
};
use cow_settlement_interface::{
data::intent::fixtures::sample_intent,
Expand Down Expand Up @@ -159,6 +164,13 @@ mod tests {
solver: pubkey_from_seed("solver"),
}
.into(),
SettlementInstruction::RemoveSolver => RemoveSolver {
program_id,
manager: payer,
rent_recipient: payer,
solver: pubkey_from_seed("solver"),
}
.into(),
}
}

Expand All @@ -177,6 +189,7 @@ mod tests {
SettlementInstruction::ReclaimBuffer,
SettlementInstruction::TransferAuthority,
SettlementInstruction::AddSolver,
SettlementInstruction::RemoveSolver,
] {
let ix = build(expected);
let accounts: Vec<_> = ix
Expand All @@ -196,6 +209,7 @@ mod tests {
ParsedInstruction::ReclaimBuffer(_) => SettlementInstruction::ReclaimBuffer,
ParsedInstruction::TransferAuthority(_) => SettlementInstruction::TransferAuthority,
ParsedInstruction::AddSolver(_) => SettlementInstruction::AddSolver,
ParsedInstruction::RemoveSolver(_) => SettlementInstruction::RemoveSolver,
};
assert_eq!(actual, expected);
}
Expand Down
104 changes: 102 additions & 2 deletions interface/src/data/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,19 @@ impl<T: Deref<Target = [u8]>> StateAccount<T> {
.checked_add(WIDTH_PUBKEY)
.ok_or(ProgramError::ArithmeticOverflow)
}

/// The account's data length after shrinking it by one solver slot: the size
/// it must be resized to once [`remove_solver`](Self::remove_solver) has
/// shifted the tail over the removed slot.
///
/// Returns [`ProgramError::ArithmeticOverflow`] if that length underflows,
/// which a caller that located a solver to remove can treat as unreachable.
pub fn shrunk_len(&self) -> Result<usize, ProgramError> {
self.0
.len()
.checked_sub(WIDTH_PUBKEY)
.ok_or(ProgramError::ArithmeticOverflow)
}
}

impl<'a> StateAccount<Ref<'a, [u8]>> {
Expand Down Expand Up @@ -258,6 +271,37 @@ impl<T: DerefMut<Target = [u8]>> StateAccount<T> {
data[gap..gap_end].copy_from_slice(&solver.to_bytes());
Ok(())
}

/// Remove `solver` from the sorted solver list, or fail with
/// [`SettlementError::SolverNotFound`] if it isn't stored.
///
/// The entries after the removed one are shifted one slot left to close the
/// gap; the now-stale trailing slot is left in place for the caller to drop
/// by resizing the account down to [`shrunk_len`](Self::shrunk_len).
pub fn remove_solver(&mut self, solver: &Pubkey) -> Result<(), ProgramError> {
let index = match self.solver_region().binary_search(&solver.to_bytes()) {
Ok(index) => index,
Err(_) => return Err(SettlementError::SolverNotFound.into()),
};

// Shift the entries after `index` down one slot; the trailing slot is
// left unchanged.
let data: &mut [u8] = &mut self.0;
let len = data.len();
let offset = WIDTH_HEADER
.checked_add(
index
.checked_mul(WIDTH_PUBKEY)
.expect("removal index bound by data length"),
)
.expect("removal offset bound by data length");
let slot_end = offset
.checked_add(WIDTH_PUBKEY)
.expect("removal slot bound by data length");

data.copy_within(slot_end..len, offset);
Ok(())
}
}

/// Test scaffolding for building state-account bytes, shared by this crate's
Expand Down Expand Up @@ -555,8 +599,6 @@ mod tests {
prop_assert_eq!(state.solvers().collect::<Vec<_>>(), expected);
}

/// `insert_solver` rejects a solver that is already stored and leaves
/// the live list untouched.
#[test]
fn insert_solver_rejects_an_existing_solver(
header in fixtures::arb_init_params(),
Expand All @@ -583,6 +625,64 @@ mod tests {
let state = StateAccount::attach(&bytes[..]).expect("valid header");
prop_assert_eq!(state.solvers().take(stored.len()).collect::<Vec<_>>(), stored);
}

#[test]
fn remove_solver_drops_a_present_solver(
header in fixtures::arb_init_params(),
// Unique and already sorted, being a `BTreeSet`.
raw_solvers in prop::collection::btree_set(any::<[u8; 32]>(), 1..50),
pick in any::<prop::sample::Index>(),
) {
let stored: Vec<Pubkey> =
raw_solvers.into_iter().map(Pubkey::new_from_array).collect();
let index = pick.index(stored.len());
let removed = stored[index];

// Remove the solver, then shrink to the length `shrunk_len`
// reports, exactly as the handler resizes the account.
let mut bytes = fixtures::state_account_bytes(&header, &stored);
let shrunk_len = StateAccount::attach(&bytes[..])
.expect("valid header")
.shrunk_len()
.expect("shrunk length fits");
prop_assert_eq!(shrunk_len, bytes.len().strict_sub(WIDTH_PUBKEY));
StateAccount::attach(&mut bytes[..])
.expect("valid header")
.remove_solver(&removed)
.expect("a present solver is removed");
bytes.truncate(shrunk_len);

let mut expected = stored;
expected.remove(index);
let state = StateAccount::attach(&bytes[..]).expect("valid header");
prop_assert_eq!(state.solvers().collect::<Vec<_>>(), expected);
prop_assert_eq!(state.solver_search(&removed), Err(index));
}

#[test]
fn remove_solver_rejects_an_absent_solver(
header in fixtures::arb_init_params(),
// Unique and already sorted, being a `BTreeSet`.
raw_solvers in prop::collection::btree_set(any::<[u8; 32]>(), 0..50),
raw_absent in any::<[u8; 32]>(),
) {
prop_assume!(!raw_solvers.contains(&raw_absent));
let stored: Vec<Pubkey> =
raw_solvers.into_iter().map(Pubkey::new_from_array).collect();
let absent = Pubkey::new_from_array(raw_absent);

let mut bytes = fixtures::state_account_bytes(&header, &stored);
prop_assert_eq!(
StateAccount::attach(&mut bytes[..])
.expect("valid header")
.remove_solver(&absent),
Err(SettlementError::SolverNotFound.into()),
);

// Nothing was removed: the stored solvers still read back in order.
let state = StateAccount::attach(&bytes[..]).expect("valid header");
prop_assert_eq!(state.solvers().collect::<Vec<_>>(), stored);
}
}
}
}
1 change: 1 addition & 0 deletions interface/src/instruction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod create_order;
pub mod initialize;
pub mod reclaim_buffer;
pub mod reclaim_order;
pub mod remove_solver;
pub mod settle;
pub mod transfer_authority;

Expand Down
Loading