Skip to content

STR 2042 mempool generalised priority ordering#1405

Open
krsnapaudel wants to merge 3 commits intomainfrom
STR-2042
Open

STR 2042 mempool generalised priority ordering#1405
krsnapaudel wants to merge 3 commits intomainfrom
STR-2042

Conversation

@krsnapaudel
Copy link
Contributor

@krsnapaudel krsnapaudel commented Feb 26, 2026

Description

Generalizes OL mempool transaction ordering behind a pluggable MempoolPriorityPolicy trait, while preserving current FIFO behavior unchanged.

MempoolPriorityPolicy trait (ordering.rs)

  • Associated type Priority: Ord + Copy + Debug + Send + Sync + 'static — used as BTreeMap keys in the priority index.
  • compute_priority(tx, txid) -> Self::Priority — computes priority from the transaction and its ID.

FifoPriority default implementation

Reproduces current timestamp-FIFO semantics via FifoPriorityKey.

Intrinsic vs extrinsic ordering (ordering.rs)

  • PackageKey::SnarkAcctUpdate(AccountId) — scaffolding for grouping same-account snark txs.
  • IntrinsicOrder with PartialOrd — comparable only within the same package (same-account seq_no).
  • FifoPriorityKey::Ord applies intrinsic order first, falls back to FIFO timestamp cross-package.

txid tiebreaker (correctness fix)

All FifoPriorityKey variants include a txid field. Without it, two transactions with the same timestamp would silently overwrite each other in the BTreeMap.

Transaction ID (types.rs)

compute_txid() uses SSZ tree-hash over canonical content (payload + attachment), excluding timestamp_micros. Manual TreeHash impls on OLMempoolSnarkAcctUpdateTxPayload and OLMempoolTxPayload match the generated pattern from .ssz types.

Generic propagation

MempoolEntry<P>, MempoolServiceState<SP, Prio>, MempoolService<SP, Prio>, MempoolBuilder<Prio> — default = FifoPriority only on MempoolBuilder (public entry point).

load_from_db()

Retains timestamp-based pre-sort for seq_no reconstruction correctness; explicitly documented as independent of the active priority policy.

This PR was created with help from Claude Code and Codex.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature/Enhancement (non-breaking change which adds functionality or enhances an existing one)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor
  • New or updated tests
  • Dependency Update

Notes to Reviewers

Is this PR addressing any specification, design doc or external reference document?

  • Yes
  • No

If yes, please add relevant links:

Checklist

  • I have performed a self-review of my code.
  • I have commented my code where necessary.
  • I have updated the documentation if needed.
  • My changes do not introduce new warnings.
  • I have added (where necessary) tests that prove my changes are effective or that my feature works.
  • New and existing tests pass with my changes.
  • I have disclosed my use of AI in the body of this PR.

Related Issues

STR-2042

@codecov
Copy link

codecov bot commented Feb 26, 2026

Codecov Report

❌ Patch coverage is 95.12195% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.90%. Comparing base (cc32d9c) to head (6502e22).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
crates/ol/mempool/src/types.rs 80.00% 14 Missing ⚠️
crates/ol/mempool/src/ordering.rs 98.40% 4 Missing ⚠️
bin/strata/src/services.rs 0.00% 1 Missing ⚠️
crates/ol/mempool/src/state.rs 98.43% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (cc32d9c) and HEAD (6502e22). Click for more details.

HEAD has 4 uploads less than BASE
Flag BASE (cc32d9c) HEAD (6502e22)
unit 3 1
functional 2 0
@@             Coverage Diff             @@
##             main    #1405       +/-   ##
===========================================
- Coverage   75.10%   64.90%   -10.21%     
===========================================
  Files         799      798        -1     
  Lines       74349    74417       +68     
===========================================
- Hits        55841    48298     -7543     
- Misses      18508    26119     +7611     
Flag Coverage Δ
functional ?
unit 64.90% <95.12%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
crates/ol/mempool/src/builder.rs 76.47% <100.00%> (-5.62%) ⬇️
crates/ol/mempool/src/handle.rs 98.01% <100.00%> (+0.05%) ⬆️
crates/ol/mempool/src/service.rs 97.05% <100.00%> (-2.95%) ⬇️
crates/ol/mempool/src/test_utils.rs 92.01% <100.00%> (+0.07%) ⬆️
bin/strata/src/services.rs 0.00% <0.00%> (-98.71%) ⬇️
crates/ol/mempool/src/state.rs 89.12% <98.43%> (-5.77%) ⬇️
crates/ol/mempool/src/ordering.rs 98.40% <98.40%> (ø)
crates/ol/mempool/src/types.rs 84.75% <80.00%> (-6.20%) ⬇️

... and 239 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 26, 2026

Commit: 3a1710f

SP1 Execution Results

program cycles success
EVM EE STF 1,320,914
Checkpoint 5,226
Checkpoint New 883,623

Copy link
Contributor

@bewakes bewakes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good in general, some comments.

Copy link
Contributor

@delbonis delbonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this should be rethought since there's a lot of modelling problems with it.

Comment on lines 229 to 247
/// Compute the transaction ID by hashing the SSZ-encoded transaction data.
///
/// This follows the established pattern: SSZ encode → SHA256 hash.
/// The hash is computed from canonical transaction content (payload + attachment), excluding
/// mempool-local metadata such as `timestamp_micros`.
///
/// The hash of [`OLMempoolTransaction`] should equal the hash of
/// [`OLTransaction`](strata_ol_chain_types_new::OLTransaction) without accumulator_proofs,
/// and also equal the hash of `RpcOLTransaction`
/// when properly converted.
pub fn compute_txid(&self) -> OLTxId {
let encoded = ssz::Encode::as_ssz_bytes(self);
let preimage = MempoolTxIdPreimage {
payload: &self.payload,
attachment: &self.attachment,
};
let encoded = ssz::Encode::as_ssz_bytes(&preimage);
let hash_bytes = hash::raw(&encoded);
OLTxId::from(hash_bytes)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a txid, don't call it a txid.

Also:

This follows the established pattern: SSZ encode → SHA256 hash.

This is not really an established pattern, we should generally be using the SSZ tree hash for things like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@delbonis I have left compute_txid in place returning OLTxId. The ID is now computed via SSZ tree-hash over canonical transaction content (payload + attachment), same hashing approach used for all other protocol types. It's a content-addressed identifier for the mempool-form transaction. Is your objection to name of method and return type or how id is computed?

Copy link
Contributor

@delbonis delbonis Mar 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My objection was that you defined a new type and then used the flat hash of that encoded message as a txid, which was completely arbitrary.

I think this new approach is fine for now. I reviewed it and retract this point.

@krsnapaudel krsnapaudel requested review from a team as code owners March 16, 2026 15:13
@krsnapaudel krsnapaudel force-pushed the STR-2042 branch 2 times, most recently from f63263c to f646252 Compare March 16, 2026 15:30
Comment on lines +146 to +171
impl<H: TreeHashDigest> TreeHash<H> for OLMempoolTxPayload {
fn tree_hash_type() -> TreeHashType {
TreeHashType::Container
}

fn tree_hash_packed_encoding(&self) -> PackedEncoding {
unreachable!("Union should never be packed")
}

fn tree_hash_packing_factor() -> usize {
unreachable!("Union should never be packed")
}

fn tree_hash_root(&self) -> H::Output {
match self {
Self::GenericAccountMessage(inner) => {
let root = <_ as TreeHash<H>>::tree_hash_root(inner);
mix_in_selector_with_hasher::<H>(&root, 0u8).expect("valid selector")
}
Self::SnarkAccountUpdate(inner) => {
let root = <_ as TreeHash<H>>::tree_hash_root(inner);
mix_in_selector_with_hasher::<H>(&root, 1u8).expect("valid selector")
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you defining tree hash on this type manually? If this is naturally SSZ-able we should be able to do #[derive(TreeHash)]. If not, then we shouldn't be implementing treehash on it in the first place and should be more careful about how the types are being modeled.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's something really weird going on here with these types. It's a pretty big red flag to me that for the mempool which treats transactions mostly as black boxes (aside from target, seqno, etc.) is defining special mempool representations of transaction types with their own serialization and treehashing. Can you see if it's possible to use the canonical transaction types, sure maybe with containers with extra metadata, or if not explain why this is necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in Slack, I will wait for a separate PR to remove mempool-specific tx types. Since they should have ssz support, we will not need manual TreeHash impls as well.

Comment on lines 206 to 216
#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)]
pub struct OLMempoolTransaction {
/// Transaction payload (generic message or snark account update).
pub payload: OLMempoolTxPayload,

/// Transaction attachment (min_slot, max_slot constraints).
pub attachment: TransactionAttachment,

/// Insertion timestamp in microseconds since UNIX epoch.
pub timestamp_micros: u64,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type's purpose is confused / at odds with itself because it contains two intrinsic components of the transaction itself alongside extrinsic information (timestamp). These are different levels of abstraction and they should be organized in a way that reflects that.

Comment on lines +280 to +284
let mut leaves = [0u8; 64];
leaves[..32].copy_from_slice(payload_root.as_ref());
leaves[32..].copy_from_slice(attachment_root.as_ref());
let root = merkle_root_with_hasher::<Sha256Hasher>(&leaves, 2);
OLTxId::from(Buf32::from(root.0))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually no, I didn't quite see that this is how this was implemented. Even if this is mathematically correct, you should just be directly calling out to the transaction types instead of reimplementing low-level hashing operations! This is how we get incompatibilities across different areas of the codebase, since different components use bespoke versions of operations like "compute txid". We should be defining "computing a txid" in one singular way, in one place, and everything else just calls that.

@krsnapaudel krsnapaudel self-assigned this Mar 18, 2026
Copy link
Contributor

@bewakes bewakes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments.

///
/// `txid` is provided for deterministic tie-breaking when two transactions otherwise share the
/// same priority.
fn compute_priority(tx: &OLMempoolTransaction, txid: OLTxId) -> Self::Priority;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason txid is being passed separately with tx? Since txid can be derived from tx, the definition of this method should only have tx as a parameter imo.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way it is now leaves room for passing one tx and a different txid not corresponding to that tx.

/// This captures only intrinsic validity linkages between transactions. Today the only intrinsic
/// linkage is per-account sequencing for snark account updates.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum PackageKey {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a package?

Comment on lines +161 to +169
Self::Snark {
timestamp_micros: t1,
txid: tx1,
..
},
Self::Snark {
timestamp_micros: t2,
txid: tx2,
..
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we define the timestamp_micros() and txid() methods in FifoPriorityKey so that this matching becomes cleaner? The destructuring is quite verbose and repititive here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants