Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7a226f6
Look up the buffers of the most traded mints at compile time
kaze-cow Sep 30, 2026
9eb5cd9
Drop the program ID check from the known buffer lookup
kaze-cow Sep 30, 2026
47648ac
Find known buffers with a perfect hash
kaze-cow Sep 30, 2026
73dc3cb
Draw slot multiplier candidates from SHA-256
kaze-cow Sep 30, 2026
9a19a6a
Merge remote-tracking branch 'origin/main' into known-buffer-pdas
kaze-cow Sep 30, 2026
19e5670
Update programs/settlement/tests/common/token.rs
kaze-cow Oct 1, 2026
fee716e
Update interface/src/pda/buffer.rs
kaze-cow Oct 1, 2026
7603ca1
Update interface/src/pda/buffer.rs
kaze-cow Oct 1, 2026
98bac60
update justfile help comment
kaze-cow Oct 2, 2026
745eeec
remove the generation script entirely, for right now we check the list
kaze-cow Oct 2, 2026
1121afe
sort addresses
kaze-cow Oct 2, 2026
fd51f2b
explain why sha256 is used
kaze-cow Oct 2, 2026
dced252
Update interface/src/pda/buffer.rs
kaze-cow Oct 2, 2026
fb76895
setup known mints to be usable by the tests
kaze-cow Oct 2, 2026
04125e1
Merge branch 'known-buffer-pdas' of github.com:cowprotocol/solana-pro…
kaze-cow Oct 2, 2026
e6825de
add descriptive comment
kaze-cow Oct 2, 2026
3a6e5f7
separate test for known buffers == known mints len
kaze-cow Oct 2, 2026
4dc726e
add test condition to verify sharing slot case
kaze-cow Oct 2, 2026
7f4e716
remove `program_id` from the arguments for validate_buffer_pda
kaze-cow Oct 2, 2026
2049d6e
re-add missing design file
kaze-cow Oct 2, 2026
3094f69
fix lint
kaze-cow Oct 2, 2026
f83b8a7
Merge remote-tracking branch 'origin/main' into known-buffer-pdas
kaze-cow Oct 2, 2026
4b699d4
better explain random source
kaze-cow Oct 2, 2026
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
25 changes: 14 additions & 11 deletions bench-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"settle/pulls_funds_to_destination": 11,
"settle/pulls_to_multiple_destinations": 12,
"settle/pushes_a_single_order": 10,
"settle/pushes_a_single_order_of_a_known_mint": 10,
"settle/pushes_several_orders_from_different_buffers": 14,
"settle/pushes_several_orders_from_one_buffer": 13,
"settle/settles_a_single_order": 10,
Expand Down Expand Up @@ -78,17 +79,18 @@
"reclaim_order/on_chain_order_partially_filled_is_not_reclaimable_before_expiry": null,
"remove_solver/remove_with_many_existing_solvers": 754,
"remove_solver/removes_a_solver": 489,
"settle/finalizes_with_no_pushes": 1106,
"settle/happy_path_sell_tokens_for_native_sol": 4626,
"settle/pulls_from_multiple_orders": 13306,
"settle/pulls_funds_to_destination": 7242,
"settle/pulls_to_multiple_destinations": 8394,
"settle/pushes_a_single_order": 6082,
"settle/pushes_several_orders_from_different_buffers": 10985,
"settle/pushes_several_orders_from_one_buffer": 10985,
"settle/settles_a_single_order": 6100,
"settle/settles_multiple_orders": 15941,
"settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7235,
"settle/finalizes_with_no_pushes": 1102,
"settle/happy_path_sell_tokens_for_native_sol": 4619,
"settle/pulls_from_multiple_orders": 13331,
"settle/pulls_funds_to_destination": 7254,
"settle/pulls_to_multiple_destinations": 8407,
"settle/pushes_a_single_order": 6095,
"settle/pushes_a_single_order_of_a_known_mint": 4584,
"settle/pushes_several_orders_from_different_buffers": 11010,
"settle/pushes_several_orders_from_one_buffer": 11015,
"settle/settles_a_single_order": 6113,
"settle/settles_multiple_orders": 15978,
"settle/settling_a_settlement_owned_order_withdraws_the_buffered_fees": 7242,
"transfer_authority/manager_can_transfer_manager": 178,
"transfer_authority/manager_can_transfer_reclaim_authority": 178,
"transfer_authority/manager_can_transfer_settlement_owned_order_authority": 177,
Expand Down Expand Up @@ -133,6 +135,7 @@
"settle/pulls_funds_to_destination": 507,
"settle/pulls_to_multiple_destinations": 548,
"settle/pushes_a_single_order": 466,
"settle/pushes_a_single_order_of_a_known_mint": 466,
"settle/pushes_several_orders_from_different_buffers": 608,
"settle/pushes_several_orders_from_one_buffer": 576,
"settle/settles_a_single_order": 466,
Expand Down
251 changes: 239 additions & 12 deletions interface/src/pda/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ use solana_pubkey::Pubkey;
use crate::pda::{is_pda_with_signer_seeds, SETTLEMENT_SEED};
use crate::SettlementError;

mod known_mints;
pub use known_mints::KNOWN_MINTS;

/// Trailing seed identifying the buffer PDAs.
pub const BUFFER_SEED: &[u8] = b"buffer";

Expand All @@ -35,7 +38,7 @@ pub const BUFFER_SEED: &[u8] = b"buffer";
/// `mint` is the raw 32-byte token mint address, so the same helper serves
/// both the off-chain builder and the on-chain handler (which holds the mint
/// as an `Address`).
pub fn buffer_pda_seeds(mint: &[u8; 32]) -> [&[u8]; 3] {
pub const fn buffer_pda_seeds(mint: &[u8; 32]) -> [&[u8]; 3] {
Comment thread
kaze-cow marked this conversation as resolved.
[SETTLEMENT_SEED, mint, BUFFER_SEED]
}

Expand All @@ -50,17 +53,151 @@ pub fn find_buffer_pda(program_id: &Pubkey, mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&buffer_pda_seeds(mint.as_array()), program_id)
}

/// A buffer PDA under [`crate::ID`] derived at compile time.
struct KnownBuffer {
mint: [u8; 32],
address: [u8; 32],
}

/// The buffers of [`known_mints::KNOWN_MINTS`], in the same order.
// Deriving this many PDAs in const eval trips the compiler's infinite-loop
// guard, although it finishes in seconds.
#[allow(long_running_const_eval)]
const KNOWN_BUFFERS: [KnownBuffer; known_mints::KNOWN_MINTS.len()] = {
let mut buffers = [const {
KnownBuffer {
mint: [0; 32],
address: [0; 32],
}
}; known_mints::KNOWN_MINTS.len()];
let mut i = 0;
while i < buffers.len() {
let mint = const_crypto::bs58::decode_pubkey(known_mints::KNOWN_MINTS[i]);
let (address, _) = const_crypto::ed25519::derive_program_address(
&buffer_pda_seeds(&mint),
crate::ID.as_array(),
);
buffers[i] = KnownBuffer { mint, address };
i += 1;
}
buffers
};

/// Bits of the hash that pick a slot: 512 slots keep collisions among 64 mints
/// The number of bits representing each input slot of `KNOWN_BUFFER_SLOTS`.
/// More bits make `KNOWN_BUFFER_SLOTS` exponentially larger, and therefore
/// increase the size of the program. However, more bits also make it easier to
/// find a multiplier with no collisions, and therefore reduce compilation
/// times.
///
/// The problem of avoiding collision is the same as the birthday paradox.
/// As a rule of thumb, finding a multiplier requires an expected `e^(nΒ²/(2m))`
/// iterations, where `n` is the number of known mints and m is 2^SLOT_BITS.
const SLOT_BITS: u32 = 9;

/// Gives every one of [`KNOWN_BUFFERS`] its own slot in [`KNOWN_BUFFER_SLOTS`].
const KNOWN_BUFFER_MULTIPLIER: u64 = find_slot_multiplier(&KNOWN_BUFFERS);

/// The index into [`KNOWN_BUFFERS`] of the mint in each slot, or `u8::MAX` for
/// an empty slot, which [`known_buffer`] then finds no buffer at.
const KNOWN_BUFFER_SLOTS: [u8; 1 << SLOT_BITS] = {
// Strictly less than MAX because the value is reserved for no buffer.
assert!(
KNOWN_BUFFERS.len() < u8::MAX as usize,
Comment thread
fedgiac marked this conversation as resolved.
"too many known mints for u8 indices"
);
let mut slots = [u8::MAX; 1 << SLOT_BITS];
let mut i = 0;
while i < KNOWN_BUFFERS.len() {
slots[slot(&KNOWN_BUFFERS[i].mint, KNOWN_BUFFER_MULTIPLIER)] = i as u8;
i += 1;
}
slots
};

/// Multiplicative hash of the leading 8 bytes of `mint`, keeping the top
/// [`SLOT_BITS`] of the product.
const fn slot(mint: &[u8; 32], multiplier: u64) -> usize {
let key = u64::from_le_bytes(*mint.first_chunk().expect("a mint is longer than 8 bytes"));
(key.wrapping_mul(multiplier) >> (u64::BITS - SLOT_BITS)) as usize
}

/// Try the multipliers `SHA-256(0)`, `SHA-256(1)`, ... (each truncated to its
/// first 8 bytes) until one sends each of `buffers` to a distinct [`slot`].
///
/// SHA-256 is utilized to provide a pseudorandom source of multipliers. Normally
/// this shouldn't be necessary as the addresses themselves should already be effectively
/// random, but its possible some of the KNOWN_MINTS use vanity addresses.
///
/// Panics (at compile time, for [`KNOWN_BUFFER_MULTIPLIER`]) if none does,
/// which is certain if two mints share their leading 8 bytes.
const fn find_slot_multiplier(buffers: &[KnownBuffer]) -> u64 {
const ATTEMPTS: u32 = 100_000;
let mut attempt: u32 = 0;
while attempt < ATTEMPTS {
let digest = const_crypto::sha2::Sha256::new()
.update(&attempt.to_le_bytes())
.finalize();
let multiplier = u64::from_le_bytes(
*digest
.first_chunk()
.expect("a digest is longer than 8 bytes"),
);
Comment thread
kaze-cow marked this conversation as resolved.
if has_distinct_slots(buffers, multiplier) {
return multiplier;
}
attempt = attempt
.checked_add(1)
.expect("attempt stays below ATTEMPTS");
}
panic!("no multiplier gives every known mint its own slot");
}

/// Whether `multiplier` sends each of `buffers` to a different [`slot`].
const fn has_distinct_slots(buffers: &[KnownBuffer], multiplier: u64) -> bool {
let mut taken = [false; 1 << SLOT_BITS];
let mut i = 0;
while i < buffers.len() {
let slot = slot(&buffers[i].mint, multiplier);
if taken[slot] {
return false;
}
taken[slot] = true;
i = i.checked_add(1).expect("i stays below the buffer count");
}
true
}

/// The compile-time buffer for `mint`, if it's one of the known mints.
fn known_buffer(mint: &[u8; 32]) -> Option<&'static KnownBuffer> {
let index = KNOWN_BUFFER_SLOTS[slot(mint, KNOWN_BUFFER_MULTIPLIER)];
KNOWN_BUFFERS
.get(usize::from(index))
.filter(|known| &known.mint == mint)
}

/// Confirm `buffer` matches the derived buffer PDA for the mint bytes `mint`
/// and `bump`.
///
/// A known mint is checked against its compile-time buffer alone, ignoring
/// `bump`: nothing signs with a buffer's seeds, so the bump only matters for
/// re-deriving the address. That buffer is derived under [`crate::ID`] whatever
/// `program_id` is, which is sound because the program only works there (see
/// [`crate::pda::state::STATE_PDA`]).
#[inline]
#[must_use = "ignoring the output means ignoring the validation result"]
pub fn validate_buffer_pda(
program_id: &Address,
buffer: &Address,
mint: &[u8; 32],
bump: u8,
) -> Result<(), ProgramError> {
is_pda_with_signer_seeds(buffer, program_id, buffer_pda_signer_seeds(mint, &[bump]))
let is_buffer = match known_buffer(mint) {
Some(known) => buffer.as_array() == &known.address,
None => {
is_pda_with_signer_seeds(buffer, &crate::ID, buffer_pda_signer_seeds(mint, &[bump]))
}
};
is_buffer
.then_some(())
.ok_or(SettlementError::PushSourceNotBuffer.into())
}
Comment thread
kaze-cow marked this conversation as resolved.
Expand Down Expand Up @@ -90,41 +227,131 @@ mod tests {

#[test]
fn accepts_a_valid_address() {
let program_id = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let (pda, bump) = find_buffer_pda(&program_id, &mint);
let (pda, bump) = find_buffer_pda(&crate::ID, &mint);

let buffer = crate::instruction::fixtures::fake_account(pda);
validate_buffer_pda(&program_id, buffer.address(), mint.as_array(), bump)
validate_buffer_pda(buffer.address(), mint.as_array(), bump)
.expect("the canonical buffer PDA must be accepted");
}

#[test]
fn rejects_an_invalid_address() {
let program_id = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let (_, bump) = find_buffer_pda(&program_id, &mint);
let (_, bump) = find_buffer_pda(&crate::ID, &mint);

// An account sitting at some other address is not the buffer.
let buffer = crate::instruction::fixtures::fake_account(Pubkey::new_unique());
let err = validate_buffer_pda(&program_id, buffer.address(), mint.as_array(), bump)
let err = validate_buffer_pda(buffer.address(), mint.as_array(), bump)
.expect_err("a non-canonical address must be rejected");
assert_eq!(err, SettlementError::PushSourceNotBuffer.into());
}

#[test]
fn rejects_a_wrong_bump() {
let program_id = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let (pda, bump) = find_buffer_pda(&program_id, &mint);
let (pda, bump) = find_buffer_pda(&crate::ID, &mint);

// The address is canonical but the carried bump doesn't derive it.
let buffer = crate::instruction::fixtures::fake_account(pda);
let err = validate_buffer_pda(&program_id, buffer.address(), mint.as_array(), bump ^ 1)
let err = validate_buffer_pda(buffer.address(), mint.as_array(), bump ^ 1)
.expect_err("a wrong bump must be rejected");
assert_eq!(err, SettlementError::PushSourceNotBuffer.into());
}

const A_KNOWN_MINT: Pubkey = Pubkey::from_str_const(KNOWN_MINTS[0]);

#[test]
fn known_buffers_len_maps_known_mints() {
assert_eq!(KNOWN_BUFFERS.len(), known_mints::KNOWN_MINTS.len());
Comment thread
kaze-cow marked this conversation as resolved.
}

#[test]
fn known_buffers_are_canonical() {
for known in &KNOWN_BUFFERS {
let (pda, _) = find_buffer_pda(&crate::ID, &Pubkey::new_from_array(known.mint));
assert_eq!(known.address, *pda.as_array());
}
}

#[test]
fn every_known_mint_looks_up_its_own_buffer() {
for mint in known_mints::KNOWN_MINTS {
let mint = Pubkey::from_str_const(mint);
let known = known_buffer(mint.as_array())
.unwrap_or_else(|| panic!("{mint} must have a known buffer"));
assert_eq!(known.mint, *mint.as_array());
}
}

#[test]
fn unknown_mint_sharing_a_slot_has_no_known_buffer() {
let mut mint = *A_KNOWN_MINT.as_array();
mint[31] ^= 1;
Comment thread
fedgiac marked this conversation as resolved.

// ensure that the slot we will hit is still resolving to the original mint
assert_ne!(
KNOWN_BUFFER_SLOTS[slot(&mint, KNOWN_BUFFER_MULTIPLIER)],
u8::MAX
);

// even so it should resolve to none
assert!(known_buffer(&mint).is_none());
}

#[test]
fn unknown_mint_in_an_empty_slot_has_no_known_buffer() {
let mint = (0u64..)
.map(|seed| *crate::fixtures::pubkey_from_seed(&seed.to_string()).as_array())
.find(|mint| KNOWN_BUFFER_SLOTS[slot(mint, KNOWN_BUFFER_MULTIPLIER)] == u8::MAX)
.expect("most slots are empty");
assert!(known_buffer(&mint).is_none());
}

/// `find_slot_multiplier` runs in a const context, where this panic is a
/// compile error; calling it at runtime is the only way to observe it.
#[test]
#[should_panic(expected = "no multiplier gives every known mint its own slot")]
fn find_slot_multiplier_rejects_mints_sharing_their_leading_bytes() {
let mut other = *A_KNOWN_MINT.as_array();
other[31] ^= 1;
let _ = find_slot_multiplier(&[
KnownBuffer {
mint: *A_KNOWN_MINT.as_array(),
address: [0; 32],
},
KnownBuffer {
mint: other,
address: [0; 32],
},
]);
}

#[test]
fn accepts_the_known_buffer() {
let (pda, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT);

validate_buffer_pda(&pda, A_KNOWN_MINT.as_array(), bump)
.expect("the known buffer PDA must be accepted");
}

#[test]
fn rejects_an_invalid_address_for_a_known_mint() {
let (_, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT);

let err = validate_buffer_pda(&Pubkey::new_unique(), A_KNOWN_MINT.as_array(), bump)
.expect_err("a non-canonical address must be rejected");
assert_eq!(err, SettlementError::PushSourceNotBuffer.into());
}

#[test]
fn ignores_the_bump_for_a_known_mint() {
let (pda, bump) = find_buffer_pda(&crate::ID, &A_KNOWN_MINT);

validate_buffer_pda(&pda, A_KNOWN_MINT.as_array(), bump ^ 1)
.expect("the known buffer PDA must be accepted whatever the bump");
}

mod proptest {
use ::proptest::prelude::*;

Expand Down
Loading
Loading