Skip to content

Mempool

@johnhenry/raijin-mempool (0.1.1) is the fee-ordered transaction pool: verify on entry (injected verifier), deduplicate by sender+nonce, order by fee for block building, evict the cheapest when full, optionally gossip accepted transactions to peers. It is the pool ValidatorNode runs, and @johnhenry/raijin-validator re-exports it.

Terminal window
npm install @johnhenry/raijin-mempool

Latest on npm: @johnhenry/[email protected].

The fee convention sits next to the tx-type byte

Section titled “The fee convention sits next to the tx-type byte”

defaultFeeExtractor reads 8 bytes of tx.data starting at offset 1, as a big-endian uint64 (data shorter than 9 bytes ⇒ fee 0n). Byte 0 is left alone because raijin-core’s state machine reads tx.data[0] as the transaction type, so a typed transaction is data = [type, fee × 8 bytes, …]. An empty-data plain transfer therefore has fee 0n under the default extractor. If your transactions carry other layouts, supply your own FeeExtractor, e.g. (tx) => tx.value for tip-style fees.

One caveat: the package README still describes the default as the first 8 bytes of data. The shipped code and type definitions read from offset 1, and that is what this page documents; confirm with defaultFeeExtractor(tx) if in doubt. (An inline comment in older type builds claims the default is tx.value; it isn’t.)

Rules that decide disputes:

  • Eviction requires strictly higher fee. When the pool is full, an incoming tx displaces the current cheapest only if its fee is strictly greater; equal fee is dropped as 'pool-full'. The displaced tx is announced as 'evicted'. Inside a validator a full pool therefore evicts its cheapest transaction instead of throwing; only a transaction that cannot out-bid it is rejected.
  • No replace-by-fee. Identity is sender+nonce, so a re-submission with the same nonce and a juicier fee is a 'duplicate', full stop.
  • A sender’s own transactions are never reordered. Block ordering ranks senders by the fee of their lowest pending nonce, so a high-fee nonce 1 can’t jump ahead of the same sender’s nonce 0 and revert on a nonce mismatch.

And a scoping fact: this is the pool a validator uses. ValidatorNode constructs it with maxSize: maxMempoolSize and a verifier built from the node’s identity.verify (the same SignatureVerifier the state machine uses), so a transaction with a bad signature is rejected at submitTransaction() instead of becoming a revert receipt one block later. Fee ordering is active inside a validator by default, so mind the fee convention above when your transactions carry typed data. Mempool imported from @johnhenry/raijin-validator is this same class; the older, simpler FIFO class of that name no longer exists.

import { Mempool, defaultFeeExtractor } from '@johnhenry/raijin-mempool'
const pool = new Mempool({
verifier: async (tx) => verifySignature(tx), // required
maxSize: 4096, // default
feeExtractor: (tx) => tx.value, // recommended override (see above)
gossip: { broadcast: (tx) => channel.send(tx) }, // optional
})
pool.onAccepted((tx) => {})
pool.onDropped((tx, reason) => {}) // 'duplicate' | 'invalid-signature' | 'pool-full' | 'evicted'
const ok = await pool.submit(tx) // boolean; accepted txs are also gossiped
const block = pool.pendingForProposer(100) // top-100 by fee, per-sender nonce order kept
Member Signature Behavior
submit (tx) → Promise<boolean> Pipeline: dedup → verify → (if full) evict-or-reject → accept → gossip. Never throws for a bad tx; watch onDropped.
remove / removeBatch (tx) → boolean / (txs) → number Delete by sender+nonce identity — call after block inclusion.
pending () → Transaction[] Fee-descending across senders, nonce-ascending within a sender (see orderByFee). Fresh array each call.
pendingForProposer (limit?) → Transaction[] Same ordering, truncated.
has / hasNonce (tx) / (sender, nonce) → boolean Membership by sender+nonce.
hasCapacityFor (tx) → boolean Cheap backpressure check with no signature verification: true while there is room, or when tx pays a strictly higher fee than the lowest pending one (so submit would evict). Added in 0.1.0 for ValidatorNode’s transaction gossip.
pruneBelowNonce (sender, currentNonce) → number Drop every pending tx from sender with a nonce below currentNonce, i.e. already consumed on-chain. Used after a state-sync import, where only the latest block’s transactions are known individually. Returns the count removed.
size getter Current count.
onAccepted / onDropped (handler) → void Synchronous event fan-out; drop handler gets the reason string.

Rejected transactions are never gossiped — the gossip hook only fires on acceptance, so a mesh of pools won’t amplify spam that fails your verifier.

The pure sorting core, exported separately. It groups transactions by sender, keeps each sender’s queue in nonce order, and lets only each queue’s head (the sender’s lowest pending nonce) compete on fee; whichever head has the highest fee is picked next (ties go to the lower nonce), exposing that sender’s next transaction. Returns a new array. Useful when you have transactions from somewhere else (another node’s gossip, a test fixture) and just need block ordering without pool semantics.

The 8-byte big-endian data rule described up top (bytes 1 through 8). Encoding a fee that way from your side:

function encodeData(type, fee) { // type byte + bigint → Uint8Array(9), fee big-endian
const b = new Uint8Array(9)
b[0] = type
for (let i = 8; i >= 1; i--) { b[i] = Number(fee & 0xffn); fee >>= 8n }
return b
}

MempoolConfig ({ verifier, maxSize?, feeExtractor?, gossip? }), FeeExtractor ((tx) => bigint), TransactionVerifier ((tx) => Promise<boolean>), GossipTransport ({ broadcast(tx) }), MempoolEvents.

Runnable: npm run example:03 walks the fee ordering, the strict-inequality eviction, and the duplicate rejection with assertions on each.