Skip to content
lkristof55Public

About

Replay any Solana block against SIMD-0649: exact fee-order priority, pecks, and a proof of how many entry batches it would need.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

peckorder

Replay any Solana block against SIMD-0649: exact fee-order priority, pecks, and a proof of how many entry batches it would need.

SIMD-0649 "Priority Ordering Within Entry Batches" (a draft, opened 2026-09-21) would make fee order a block validity rule: inside every entry batch, a leader must record transactions in non-increasing priority = reward * 1,000,000 / (requested_cost + 1), and every batch except the last must span at least two FEC sets. peckorder takes one getBlock response, recomputes that priority for every transaction with the SIMD's exact integer formula, counts where a lower payer was recorded right before a higher one (a peck), and proves a lower bound on how many batches and data shreds the block would need to be valid. It checks its own cost model against the chain on every transaction it prices.

Nobody is breaking a rule today. The SIMD is a draft; every verdict here says would be, and it is pinned to commit cc4ec63056.

TypeScript, zero runtime dependencies (own base58, BigInt u64 math), runs on Node >= 23.6 without a build step. MIT.

$ peckorder slot 450356456
peckorder · SIMD-0649 (draft, cc4ec63056) · slot 450356456
  leader          (pSo1KZ…nkfc)
  transactions    930 (661 votes exempt, 269 priced)
  pecks           119 out of order (44.2 per 100)
  needs           120 entry batches, 7,648 data shreds
  its bytes fill  9 batches (502,076 B at 61,632 B each)
  verdict         DEAD AT FULL PACKING, 13.33x over
  to pass         its shreds would average <= 65.6 B of 963 B
  worst peck      48,278.1x (23,415 then 1,130,430,608, position 158)
  kendall tau-a   0.1516 (15,155 inversions)
  jito tips       11 tipped; counting tips as reward: 118 pecks (not the SIMD rule)
  self-check      cost model 269/269, fee 930/930 matched the chain
  under SIMD-0649 (draft) this block would be dead. nobody is breaking a rule today.

Install and run

Not on npm yet (published: false; the package name peckorder was free on 2026-09-25). From a clone:

git clone https://github.com/lkristof55/peckorder && cd peckorder
npm install                                # runs `prepare`: builds dist/ (the package entry and the bin)
npm test                                   # offline, recorded mainnet fixtures
node src/cli.ts file test/fixtures/block-450356456.json.gz --slot 450356456     # offline
PECKORDER_RPC_URL=https://your-rpc node src/cli.ts slot 450356456               # live, 1 getBlock
node src/cli.ts tx <signature>             # did a lower payer get recorded ahead of you?
node src/cli.ts leader <validator identity>   # its last complete 4-slot window
node src/cli.ts vectors <slot> > v.jsonl   # conformance vectors

dist/ is not committed. import 'peckorder' and the peckorder bin resolve to dist/*.js, because Node will not strip types inside node_modules. npm install in a clone, npm pack, and npm i github:lkristof55/peckorder all run prepare, which builds it with Node's own type stripper (npm run build does the same by hand). node src/cli.ts and import './src/index.ts' need no build. After that the same commands run as npx peckorder …. The RPC is --rpc, then $PECKORDER_RPC_URL, then https://api.mainnet-beta.solana.com. The public endpoint throttles getBlock; any RPC with block history works. Every call is read-only.

Use it as a library

import { auditBlock, fetchBlock, locateTx } from 'peckorder';

const rpc = process.env.PECKORDER_RPC_URL!;
const block = await fetchBlock(rpc, 450356456); // getBlock json, maxSupportedTransactionVersion 1
const a = auditBlock(block, { slot: 450356456 });

a.ascents;             // 119 pecks
a.batchesNeeded;       // 120 = pecks + 1
a.verdict;             // 'dead-at-full-packing'
a.checks;              // { cost: { ok: 269, mismatch: 0 }, fee: { ok: 930, mismatch: 0 }, unpriced: 0 }
a.pins[158];           // { sig, priority: 23415, reward, cost, plate, ascent, offset, wire, ... }
locateTx(a.pins, sig); // { position, rank, aheadLower, nearestAheadLower, behindHigher, ... }

How it works

getBlock(slot) ──► for each tx ─────────────────────────────────────────────────────────┐
                    │ simple vote? (< 3 sigs, legacy, 1 ix, Vote program) ── exempt      │
                    │ requested cost  = 720·sigs + precompile verify                     │
                    │                 + 300·requested write locks (no demotion)          │
                    │                 + ix-data bytes / 4                                │
                    │                 + requested CU limit + 8·⌈loaded-data limit / 32K⌉ │
                    │ leader reward   = prioritization fee + base fee − ⌊base fee·50/100⌋│
                    │ priority        = min(reward·10⁶, 2⁶⁴−1) / (cost + 1)    (u64)     │
                    │ wire size       = exact serialized bytes (legacy / v0 / v1)        │
                    │ self-check      = meta.fee, meta.costUnits                         │
                    └────────────────────────────────────────────────────────────────────┘
non-vote priorities in recorded order:  p0 ≥ p1 ≥ p2 < p3 ≥ p4 < p5 …
                                                   ▲ peck    ▲ peck        k = # pecks
each batch must be non-increasing  ─►  every peck is a batch boundary  ─►  batches ≥ k + 1
every non-final batch ≥ 64 data shreds, the last ≥ 32 (one FEC set) ─►  shreds  ≥ 64k + 32
entry bytes ≤ Σ wire + 56·(txs + 64 ticks)  ─►  at 963 B/shred they fill ⌊bytes / 61,632⌋ + 1 batches

1. Exempt votes. A simple vote is Agave's is_simple_vote_transaction: fewer than 3 signatures, a legacy message, exactly one instruction, to the Vote program. The SIMD exempts exactly these. Every other transaction, failed ones included, stays in the sequence.

2. Requested cost is the value the SIMD names: the pre-execution cost the Agave cost model reserves.

component value source
signatures 720 per tx signature, plus 6,690 / 2,400 / 4,800 per secp256k1 / ed25519 / secp256r1 signature (byte 0 of each precompile instruction) block_cost_limits.rs
write locks 300 per requested write lock: header-writable static keys + ALT-writable keys, no demotion test_cost_model_demoted_write_lock
instruction data total instruction-data bytes / 4 INSTRUCTION_DATA_BYTES_COST
execution legacy/v0: SetComputeUnitLimit, else 3,000 per builtin ix + 200,000 per other ix, cap 1,400,000. v1: transactionConfig.computeUnitLimit, 0 if unset compute-budget, SIMD-0385
loaded data 8 per 32 KiB page of the requested limit. legacy/v0 default 64 MiB (16,384 CU); v1 unset = 1 page cost model, SIMD-0385

3. Leader reward = prioritization fee + base fee − ⌊base fee × 50 / 100⌋, with base fee = 5,000 × (signatures + precompile signatures). The prioritization fee is ⌈µlamports/CU × CU limit / 10⁶⌉ for legacy/v0 and transactionConfig.priorityFee for v1. Jito tips are not reward under the SIMD; peckorder reports them separately (tip, tipAdjustedAscents) for contrast only.

4. Priority = min(reward × 1,000,000, 2⁶⁴ − 1) / (cost + 1), integer division, all in BigInt.

5. Order. Ascents (pecks) are i with p[i] < p[i+1]; equal priorities are allowed. Inversions (i < j, p[i] < p[j]) are counted with a bottom-up merge sort in O(n log n), exactly (float64 when every value is below 2⁵³, BigInt otherwise). Kendall tau-a against the non-increasing order is (concordant − discordant) / C(n,2), ties count as neither.

6. The lower bound. Every batch must be non-increasing, so every peck must fall on a batch boundary: batches ≥ k + 1. Every non-final batch is at least 64 data shreds and the last is at least one FEC set, so data shreds ≥ 64k + 32. This is certain; it needs no knowledge of where the real boundaries are.

7. The byte bound. Entry bytes ≤ Σ exact wire sizes + 56 B per transaction and per tick (8 B num_hashes + 32 B hash + 8 B tx-vector length + 8 B slack; entries ≤ txs + 64 ticks). Wire sizes are exact: compact-u16 layout for legacy/v0, and the fixed SIMD-0385 layout for v1 (version, header, u32 config mask, lifetime, counts, addresses, 4 B per config word, 4 B per instruction header, payloads, signatures last). On slot 450355700 they equal encoding=base64 byte for byte on all 1,839 transactions (legacy 889, v0 310, v1 640).

8. The verdict.

verdict condition strength
dead-by-shred-cap 64k + 32 > 32,768 (Agave DEFAULT_MAX_DATA_SHREDS_PER_SLOT), i.e. k ≥ 512 a proof from protocol constants alone
dead-at-full-packing k + 1 > ⌊entry bytes / 61,632⌋ + 1 assumes shreds carry entry data as full as both clients pack them today (963 B per chained, unsigned merkle data shred)
could-pass otherwise might be valid if the real, unexposed boundaries fall on its pecks

maxAvgBytesPerShred = entry bytes / (64k + 32) says how empty the shreds would have to be for the block to pass.

9. Self-check, on every block. meta.fee must equal base fee + prioritization fee (checks the CU limit, price and precompile parsing). meta.costUnits − signature − write-lock − data-bytes − computeUnitsConsumed must be a positive multiple of 8 (costUnits is the executed cost, which swaps in actual CU and actual loaded data, so the static parts must match exactly). A mismatch shows up in checks, never silently.

The model cut. pins[].plate = ⌊offset / 61,632⌋ lays the two-FEC-set size over the byte stream. It is a model, not the real entry batches: RPC does not expose batch boundaries.

API

export what
auditBlock(block, { slot?, leader?, leaderNames? }) the full audit: summary fields, worstPecks, pins (one per non-vote tx, recorded order), rule
auditBlockDetailed(block, opts) the audit plus every priced tx (votes included) in BigInt
priceTransaction(tx) { sig, version, vote, priority, reward, cost, tip, failed, wire, feeOk, costOk } (BigInt)
requestedCost(tx), leaderReward(tx), priority(reward, cost), wireSize(tx), isSimpleVote(tx) the pieces
countAscents, countInversions, countInversionsNaive, countTiedPairs, kendallTauA order metrics over bigint[]
verdictOf(k, bytes), batchesNeeded, shredsNeeded, batchesAtFullPacking the bounds
locateTx(pins, sig), locateVote(priced, nonVote) where one tx landed: rank, lower payers ahead, higher payers behind
leaderWindows(relativeSlots, epochStart, tip), lastCompleteWindowStart(tip) 4-slot leader windows from getLeaderSchedule
fetchBlock, fetchFinalizedSlot, fetchSignatureStatus, fetchEpochInfo, fetchLeaderSlots, fetchSlotLeader, rpcCall a tiny read-only JSON-RPC client (timeout, backoff on 429)
toVectors(block, slot), formatCard(audit), formatTx(loc, slot) conformance vectors and the text card
RULE and every constant src/rule.ts, one line of source per value

Benchmarks

Measured with npm run bench on the recorded fixtures (Apple M5, 16 GB, Node v26.8.1, darwin/arm64, 2026-09-25):

what result note
auditBlock() slot 450355700, 1,839 txs 42.7 ms median of 20 JSON.parse of the 3.6 MB trimmed response: 3.7 ms more
priceTransaction() over the 2,769 recorded txs 50,522 txs/s cost + reward + priority + wire size + both self-checks
countInversions() 1,000,000 random priorities 50 ms median of 5 O(n log n) merge sort, exact (float64 path, values < 2^53)
countInversions() 200,000 priorities above 2^53 27 ms median of 3 BigInt fallback path
inversions on 20,000: naive O(n^2) vs merge sort 328 ms vs 2.8 ms 116x
self-check vs meta.fee (all txs) 2,769/2,769 base + prioritization fee
self-check vs meta.costUnits (non-vote) 1,392/1,392 signature + write-lock + data-bytes components

The fixtures are trimmed getBlock responses (logs, inner instructions and token balances removed); a full response for this block is 8.7 MB of JSON, so parsing one live costs more than the 3.7 ms above.

What it found (2026-09-25)

  • The two recorded blocks reproduce an independent Python prototype field for field (test/fixtures/expected-prototype.json): slot 450356456 has 119 pecks and a worst peck of 48,278.1x; slot 450355700 has 264 pecks, needs 265 batches, and its bytes fill 25.
  • A live run of the CLI on slot 450370363 (leader 9eGrDo…8FoY): 475 pecks in 1,069 priced txs, needs 476 batches and 30,432 data shreds, its bytes fill 20 batches: under the draft it would be dead at full packing, 23.8x over, with a 96,622.7x worst peck. Cost model 1,069/1,069 and fee 1,757/1,757 matched the chain.
  • Counting Jito tips as reward barely moves the disorder (118 vs 119 pecks, 466 vs 475): tips do not explain it.

Conformance vectors

SIMD-0649 asks that every client compute priority identically. vectors/*.jsonl has one line per non-vote transaction of the recorded blocks (1,392 lines), each confirmed by the chain's own meta.fee and meta.costUnits:

{"slot":450356456,"index":0,"sig":"4PzJw6…","version":"legacy","requestedCost":"217710","reward":"2500","priority":"11483","feeCheck":true,"costCheck":true}

peckorder vectors <slot> produces the same for any slot.

Limits and PLANNED

  • Draft rule. Constants are pinned to SIMD-0649 at cc4ec63056. If the SIMD changes, src/rule.ts changes.
  • Real batch boundaries are not in RPC (DATA_COMPLETE_SHRED flags live in shreds). Only the lower bound (k + 1 batches, 64k + 32 shreds) is certain. PLANNED: an exact verdict from a shred or entries feed.
  • Requested loaded-data cost cannot be cross-checked against RPC; it follows the Agave source and SIMD-0385.
  • Migrated builtins. Without SetComputeUnitLimit, Stake, Config and Address Lookup Table instructions are counted at 200,000 (assumed migrated to Core BPF). The fee check confirms the default for System and ComputeBudget (17 recorded txs); the migrated case did not occur in the fixtures.
  • Slots before 447,120,000 (epoch 1035) are refused by the CLI: the pinned fee and cost rules and tx v1 were not all live.
  • PLANNED: per-client and per-scheduler attribution; a continuous vector exporter that diffs Agave and Firedancer once the SIMD's shared vectors exist. There is no on-chain program in any version.

Prior art

  • Jito IBRL Explorer scores slot time, vote packing and compute spread across ticks, not fee order.
  • sandwiched.me tracks sandwich attacks per validator.
  • Solscan / Solana Explorer show a transaction's index in its block, not its priority.
  • Helius getPriorityFeeEstimate and xroot.dev's formula write-up estimate or explain the ranking; they do not replay blocks.
  • A GitHub search for SIMD-0649, "priority ordering solana" and "solana block ordering" found no repos (2026-09-25).

peckorder replays SIMD-0649 on real blocks with the exact priority, a provable batch lower bound, and a per-block self-check against the chain.

Tests

npm test          # 30 tests, offline: node --test on recorded mainnet fixtures
npm run typecheck # tsc --noEmit, strict (dev dependency)
npm run bench
PECKORDER_RPC_URL=… node scripts/record.ts 450355700 450356456   # re-record fixtures (read-only)

The fixtures are two getBlock responses from 2026-09-25, trimmed to the fields peckorder reads, plus the exact wire sizes of slot 450355700 from encoding=base64.

The live app

app/ is the complete source of peckorder.netlify.app: the site (three.js, the block as plates and pins) and the Netlify Functions behind /api/recent, /api/slot, /api/tx, /api/leader and /api/leaderboard, plus a sampler that audits the latest leader window every 5 minutes. The functions import this library from src/ directly, so the live demo is the library.

cd app && npm ci && npm test         # offline
cp .env.example .env                 # set HELIUS_API_KEY (or SOLANA_RPC_URL)
npm run build && npm run dev         # http://localhost:8888

To deploy your own copy, create a Netlify site from a fork: the root netlify.toml builds app/, and the only required env var is HELIUS_API_KEY. Endpoints, env vars, the schedule and the credit budget are in app/README.md.

License

MIT. Copyright (c) 2026 Peckorder contributors. The Peckorder site runs this library as its live demo.

About

Replay any Solana block against SIMD-0649: exact fee-order priority, pecks, and a proof of how many entry batches it would need.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages