Repository navigation
Look up the buffers of the most traded mints at compile time - #175
Conversation
`validate_buffer_pda` now checks a compile-time table of the buffer PDAs for the 63 most traded verified tokens before falling back to `create_program_address`, saving ~1430 CU per push of a known buy mint at the cost of ~76 CU per push of any other. `just generate-known-mints` refreshes the mint list from Jupiter's verified token list, ranked by 24h volume. The buffer addresses are derived from it with const-crypto, like `STATE_PDA`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The settlement program only works at `crate::ID` (`Initialize` fails elsewhere because of `STATE_PDA`), so checking `program_id` before the known buffer lookup costs 18 CU per push for nothing. Also check that every known mint looks up its own entry and canonical address, not just some entry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The known mints are fixed at compile time, so a const block searches for a multiplier that sends each one to its own slot of a 256-entry index table. A lookup is now one multiply, shift and load plus the full mint comparison, instead of a 7-comparison binary search: 42 CU cheaper for known and unknown mints alike, and 744 bytes smaller. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hand-rolled splitmix64 was hard to review without knowing it. The candidates are now the leading 8 bytes of SHA-256(0), SHA-256(1), ..., through the const-crypto dependency we already have. SHA-256 is much slower to evaluate at compile time, so the slot table grows to 512 entries: today's mints then need 41 candidates instead of 1,364, keeping the build free of long-running const eval warnings at a cost of 256 bytes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fedgiac
left a comment
There was a problem hiding this comment.
Extremely interesting hack, I really like the idea and how it turned out. I really want to merge this! const is fun. 😄
The only design change I'm considering right now (but I'm not sure if it's actually better than the current design) is generating the buffers and the multiplier through the same script that generates the mints.
This moves compilation times (basically) back to what they were before. However, the drawback is that the generation script needs to be rerun on every minor or major version bump.
Compilation time still seems under control, so maybe it's better to keep it as it is.
That was my original thought too, but then seeing what the AI was able to accomplish with just const generation, it seemed best for clarity to keep the generation script simple. The compilation times are low enough (and we are close enough to the end of the project) that it doesn't seem. I don't know why, but I just want to avoid having to make a lot of bespoke generation code If we still have concerns, I actually would rather try moving all of this logic into an isolated cargo crate, which would mean the crate is only need to be rebuilt when the generation changes. |
Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
…grams into known-buffer-pdas
# Description Moves the effective buffer account from the state PDA to what the buffer account *would* be if the system program was a real mint. ## Motivation Using the state PDA as the source buffer means that the state PDA also needs to be declared as a writable account (rather than readable, as it currently is). This effectively prevents two otherwise unrelated settlement transactions from being included in parallel on a single solana block. ## Considerations ### The token source buffer Before this needed to be set by the solver to the state PDA. Originally I really liked the idea of having the buffer match with the usual buffer_pda pattern, but with recent suggestions from @fedgiac , this may be more of a footgun. So now it uses a completely indepedently derived seed. ### Creating the buffer account Unlike other buffer accounts, what we need is an empty PDA at the derived address. One option is to do it as part of `CreateBuffers`. This would basically come down to effectively returning early after running the `CanonicalPda` call to create the necessary account, and modifying the account length. It could also be done in the `Initialize`, but this is a breaking change since the new account needs to be supplied and existing settlement programs cannot call Initialize twice, but it is the most natural option considering the program lifecycle. This is the option used in this PR. ### Piggybacking off of #175 #175 made it possible to look up from a small list of "known" mints the correct buffer pda. We can use this exact same lookup for the native buffer as well, since it uses the same code path. This means we no longer need `is_native_sol` for a comparison anywhere in the codebase, and we use the same perfect hash to lookup the correct native buffer, simplifying the code paths in some ways. ## Validating the Native SOL buffer Since the native SOL buffer now derives the same way as any other buffer, we can remove the if statement that does the validation of the buffer. I decided not to do this ## How to test Read the above methodology and check the changes. They are mostly refactoring/renaming from `STATE_PDA` to `NATIVE_SOL_BUFFER_PDA`. --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Federico Giacon <58218759+fedgiac@users.noreply.github.com>
BeginSettlere-derives the buffer PDA of every push's buy mint withcreate_program_address, which costs 1500 CU per order. Most volume is in a few tokens, so this derives their buffers at compile time instead, the same waySTATE_PDAis.What changes
validate_buffer_pdafirst looks the buy mint up in a compile-time table of the buffer PDAs for the 63 most traded verified tokens. It falls back to the current derivation when the mint isn't in the table.interface/src/pda/buffer/known_mints.rsholds a list of mints that should be pre-indexed for the table. (see the script below for an example of how this file was originally generated. the exact mints we choose to put in this file is out of scope).const_crypto, so a version bump carries through without regenerating. Deriving this many PDAs tripslong_running_const_eval, so the lint is allowed on that one constant.u8::MAX, which points past the end of the buffer list, so a miss needs no extra check. If no multiplier works, the build fails; that's certain if two mints share their leading 8 bytes.crate::ID(Initializefails elsewhere because ofSTATE_PDA), so known mints are checked againstcrate::ID's buffers whateverprogram_idis. The fallback derivation still usesprogram_id.63 mints were chosen based on the table size analysis below and, by arbitrary design, its
2**n - 1number, which can be useful for many data structures in computing. One additional account will be added relating to #174 separately, bringingthe total to 64. The number is honsetly somewhat arbitrary now that we are not using a binary search to lookup the desired value, but it feels nice as a programmer 😅On using a hashing library
It would be really nice if we didn't have to have the code for
find_slot_multiplier, but there don't seem to be any great librariesResults
mainpushes_a_single_order_of_a_known_mint)pushes_a_single_order)cow_settlement.soThe fast path pays for itself once about 1% of pushes are for known mints.
Commits:
7a226f6adds the table with a binary search (4636 / 6143 CU).9eb5cd9drops the program ID check (4618 / 6125 CU).47648acswitches to the perfect hash (4576 / 6082 CU).Picking the table size
This sweep was run on the first commit's binary search, which still had the program ID check and stored bumps. The size, rent and build-time columns still describe how the table scales. The CU columns are higher than with the perfect hash, whose cost doesn't grow with the table.
All sizes use the same Jupiter snapshot, and "vs main" is against
main'svalidate_buffer_pda. Volume share is each size's share of 24h volume across Jupiter's verified tokens. Build time is how long it takes to runjust buildto the nearest second..sobytes vs main63 mints covered 98.3% of volume in that snapshot on Jupiter.
Out of scope
Jupiter may not be the best source of generating this list of tokens. We can revamp the list, or even count, prior to launch upon further discussion with the team.
The following example script was used to generate the initial list of 63 accounts as a starting point:
Test plan
🤖 Generated with Claude Code