Skip to content

Conversation

@insipx
Copy link
Contributor

@insipx insipx commented Dec 1, 2025

Add child resolution and ICE handling by extending CursorStore with ice and resolve_children across in-memory, SQL, and wrapper implementations

Extend CursorStore with ice and resolve_children, add an in-memory icebox in InMemoryCursorStore, delegate persistence/lookup in the SQLite store, and switch GlobalCursor to BTreeMap with merge via apply. Update callers to construct and return GlobalCursor directly.

📍Where to Start

Start with the trait changes in cursor_store.rs, then review the in-memory implementation in in_memory_cursor_store.rs and the SQLite store in cursor_store.rs.


📊 Macroscope summarized 0972aaf. 9 files reviewed, 6 issues evaluated, 6 issues filtered, 0 comments posted

🗂️ Filtered Issues

xmtp_db/src/encrypted_store/refresh_state.rs — 0 comments posted, 2 evaluated, 2 filtered
  • line 133: rows_to_global_cursor_map performs unchecked signed-to-unsigned integer casts that can silently wrap negative database values, yielding incorrect OriginatorId and SequenceId. Specifically, originator_id as u32 and sequence_id.unwrap_or(0) as u64 will wrap if originator_id or sequence_id are negative, producing large unsigned values instead of rejecting invalid input. This can corrupt the resulting GlobalCursor (e.g., mapping to the wrong originator or an enormous sequence), leading to incorrect cursor computations downstream. Validate non-negativity before casting, or return an error on invalid rows. [ Previously rejected ]
  • line 706: latest_cursor_combined may generate invalid SQL when entities contains no kinds that map to group_messages (i.e., neither EntityKind::ApplicationMessage nor EntityKind::CommitMessage). In that case group_message_kinds becomes an empty Vec, producing kind IN () in the constructed SQL, which is a syntax error in SQLite. This is reachable if callers pass, e.g., only EntityKind::Welcome. Ensure you guard against an empty group_message_kinds (e.g., omit the group_messages branch or use a false predicate) before constructing the placeholders. [ Out of scope ]
xmtp_mls/src/cursor_store.rs — 0 comments posted, 4 evaluated, 4 filtered
  • line 70: sid from the database is cast to u64 with as without validating non-negativity. If sid is negative (e.g., due to DB corruption or unexpected input), it will wrap to a very large u64, corrupting the cursor and causing incorrect behavior (e.g., dominance/missing comparisons). Validate sid >= 0 and return an error or clamp, or change the DB/API to return an unsigned type. [ Previously rejected ]
  • line 101: sid is cast to u64 without validating non-negativity in latest_per_originator for identity updates, which can wrap negatives to huge values and corrupt cursor semantics. Validate or use an unsigned type. [ Previously rejected ]
  • line 163: Potential integer wrap: sid from get_latest_sequence_id_for_inbox(...) is cast with as u64 without validation. If sid is negative, the cast will wrap to a very large u64, producing an incorrect GlobalCursor value and potentially causing downstream logic to skip messages. Validate that sid >= 0 and either clamp, normalize, or return an error before inserting into the GlobalCursor. Affected line: map.insert(Originators::INBOX_LOG, sid as u64);. [ Previously rejected ]
  • line 163: sid is cast to u64 without validation in latest_for_topics for IdentityUpdatesV1, which can wrap negative SIDs and produce incorrect GlobalCursor values. Validate sid or return an error if negative. [ Previously rejected ]

@insipx
Copy link
Contributor Author

insipx commented Dec 1, 2025

@claude
Copy link

claude bot commented Dec 1, 2025

Claude finished @insipx's task —— View job


PR Review - December 11, 2025

Critical Issues

1. False positive in OrphanedEnvelope::is_child_of (xmtp_proto/src/types/orphaned_envelope.rs:33-36)

The method uses GlobalCursor::get() which returns 0 for missing originators. When cursor.sequence_id == 0, this creates false positives:

pub fn is_child_of(&self, cursor: &Cursor) -> bool {
    self.depends_on.contains_key(&cursor.originator_id)
        && self.depends_on.get(&cursor.originator_id) == cursor.sequence_id
}

If an orphan has no dependency for a given originator, get() returns 0. When the cursor's sequence_id is also 0, the comparison succeeds incorrectly. This affects InMemoryCursorStore::resolve_children at line 124, potentially removing unrelated envelopes from the icebox.

Fix: Check key existence explicitly:

pub fn is_child_of(&self, cursor: &Cursor) -> bool {
    self.depends_on.get(&cursor.originator_id) == cursor.sequence_id
        && self.depends_on.contains_key(&cursor.originator_id)
}

2. SQL error in lowest_common_cursor_combined (xmtp_db/src/encrypted_store/refresh_state.rs:563-569)

The query references conversation_list with columns that don't exist in that table (originator_id, sequence_id, message_kind). This will fail at runtime. Based on the comment mapping and other code, this should query group_messages instead.

Test Coverage Concerns

  • 20% patch coverage with 135 untested lines is concerning for core cursor logic
  • New trait methods ice() and resolve_children() lack coverage in:
    • InMemoryCursorStore (xmtp_api_d14n/src/protocol/in_memory_cursor_store.rs:111-130)
    • SqliteCursorStore wrapper (xmtp_mls/src/cursor_store.rs:195-210)
    • All trait impls for Option<T>, &T, Arc<T>, Box<T> (xmtp_api_d14n/src/protocol/traits/cursor_store.rs:159-177)
  • The database implementation in xmtp_db/src/encrypted_store/icebox.rs has good test coverage, but the higher-level abstractions do not

Additional Issues

3. Missing test for sequence_id=0 edge case

Given the false positive bug, there should be explicit tests covering:

  • Orphans with dependencies on sequence_id 0
  • Resolution attempts with cursor sequence_id 0
  • Empty GlobalCursor behavior in is_child_of

4. GlobalCursor refactor looks good

The HashMap → BTreeMap change is sensible for deterministic ordering and Hash derivation. The new apply() method is a clean addition.

Recommendations

  1. Fix the is_child_of false positive bug (high priority)
  2. Fix the SQL table reference bug (high priority)
  3. Add integration tests exercising ice() and resolve_children() through the full stack
  4. Add edge case tests for sequence_id=0 scenarios
  5. Consider property-based tests for orphan resolution logic given its complexity

Review completed at 2025-12-11 21:04 UTC

#[derive(Default, Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq)]
pub struct GlobalCursor {
inner: HashMap<OriginatorId, SequenceId>,
inner: BTreeMap<OriginatorId, SequenceId>,
Copy link
Contributor Author

@insipx insipx Dec 1, 2025

Choose a reason for hiding this comment

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

changed the backing data structure from a HashMap to a BTreeMap. The BTreeMap makes more sense to me, since we can order the map by keys and also derive Hash on it. the semantics of inserting/"contains" etc. is unchanged

@insipx insipx force-pushed the insipx/extractors branch from 71ae3cb to d0c6474 Compare December 2, 2025 14:59
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from 3fa421b to 1829d0a Compare December 2, 2025 14:59
@codecov
Copy link

codecov bot commented Dec 2, 2025

Codecov Report

❌ Patch coverage is 20.11834% with 135 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.49%. Comparing base (fc5cf12) to head (0972aaf).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
xmtp_api_d14n/src/protocol/traits/cursor_store.rs 0.00% 77 Missing ⚠️
xmtp_mls/src/cursor_store.rs 0.00% 21 Missing ⚠️
...tp_api_d14n/src/protocol/in_memory_cursor_store.rs 13.63% 19 Missing ⚠️
...i_d14n/src/queries/d14n/test/send_group_message.rs 0.00% 10 Missing ⚠️
xmtp_proto/src/types/orphaned_envelope.rs 0.00% 4 Missing ⚠️
xmtp_db/src/encrypted_store/refresh_state.rs 70.00% 3 Missing ⚠️
xmtp_proto/src/types/global_cursor.rs 95.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2869      +/-   ##
==========================================
- Coverage   73.67%   73.49%   -0.18%     
==========================================
  Files         402      402              
  Lines       51098    51237     +139     
==========================================
+ Hits        37644    37656      +12     
- Misses      13454    13581     +127     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@insipx insipx force-pushed the insipx/cursor-store-icebox branch from 1829d0a to 47c44da Compare December 2, 2025 15:28
@insipx insipx force-pushed the insipx/extractors branch from d0c6474 to 2a525e0 Compare December 2, 2025 15:28
@insipx insipx changed the title resolve children & ice envelopes resolve children & ice envelopes to CursorStore Dec 2, 2025
@insipx insipx force-pushed the insipx/extractors branch from 2a525e0 to 4aa9e3f Compare December 3, 2025 22:15
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from 47c44da to a81a59b Compare December 3, 2025 22:15
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from a81a59b to da907f5 Compare December 3, 2025 22:37
@insipx insipx force-pushed the insipx/extractors branch 2 times, most recently from 40bd8fd to 4937bec Compare December 3, 2025 22:38
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from da907f5 to c5e17ad Compare December 3, 2025 22:38
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from 7456421 to c7928f5 Compare December 10, 2025 20:57
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from c7928f5 to 32e6075 Compare December 10, 2025 21:02
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from 32e6075 to 763c26d Compare December 10, 2025 21:26
@insipx insipx force-pushed the insipx/extractors branch 2 times, most recently from 0160848 to 17e7833 Compare December 10, 2025 21:28
@insipx insipx force-pushed the insipx/cursor-store-icebox branch 2 times, most recently from 12512c0 to 2e0df41 Compare December 10, 2025 21:38
@insipx insipx force-pushed the insipx/extractors branch 2 times, most recently from 058f33b to e05e925 Compare December 10, 2025 21:39
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from 2e0df41 to c49f3ba Compare December 10, 2025 21:39
Base automatically changed from insipx/extractors to main December 10, 2025 21:58
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from c49f3ba to e3540d3 Compare December 10, 2025 22:04
@insipx insipx changed the title resolve children & ice envelopes to CursorStore add children resolution & ice to CursorStore trait Dec 10, 2025
@insipx insipx force-pushed the insipx/cursor-store-icebox branch from e3540d3 to 7a08a24 Compare December 11, 2025 16:05
@insipx insipx changed the title add children resolution & ice to CursorStore trait add child resolution & ice to CursorStore trait Dec 11, 2025
@insipx insipx force-pushed the insipx/cursor-store-icebox branch 2 times, most recently from e626807 to 35ec6e7 Compare December 11, 2025 20:42
@insipx insipx merged commit 0949b7d into main Dec 15, 2025
18 checks passed
@insipx insipx deleted the insipx/cursor-store-icebox branch December 15, 2025 17:16
rygine pushed a commit that referenced this pull request Dec 28, 2025
<!-- Macroscope's pull request summary starts here -->
<!-- Macroscope will only edit the content between these invisible
markers, and the markers themselves will not be visible in the GitHub
rendered markdown. -->
<!-- If you delete either of the start / end markers from your PR's
description, Macroscope will post its summary as a comment. -->
### Add child resolution and ICE handling by extending `CursorStore`
with `ice` and `resolve_children` across in-memory, SQL, and wrapper
implementations
Extend `CursorStore` with `ice` and `resolve_children`, add an in-memory
icebox in `InMemoryCursorStore`, delegate persistence/lookup in the
SQLite store, and switch `GlobalCursor` to `BTreeMap` with merge via
`apply`. Update callers to construct and return `GlobalCursor` directly.

#### 📍Where to Start
Start with the trait changes in
[cursor_store.rs](https://github.com/xmtp/libxmtp/pull/2869/files#diff-028daad07b57a99602b70e297432f143b84633ff46fa3f2b9851f6b476260eae),
then review the in-memory implementation in
[in_memory_cursor_store.rs](https://github.com/xmtp/libxmtp/pull/2869/files#diff-81e05c4cbc4c5877da996a1302449b85dde8e85a96338b80953cbc520363c6c2)
and the SQLite store in
[cursor_store.rs](https://github.com/xmtp/libxmtp/pull/2869/files#diff-92939812065b708096075a08e83053a96bbb0d48d2a13d665eba3336c85740ca).

----
<!-- Macroscope's review summary starts here -->

<details>
<summary>📊 <a href="https://app.macroscope.com">Macroscope</a>
summarized 0972aaf. 9 files reviewed, 6 issues evaluated, 6 issues
filtered, 0 comments posted</summary>

### 🗂️ Filtered Issues
<details>
<summary>xmtp_db/src/encrypted_store/refresh_state.rs — 0 comments
posted, 2 evaluated, 2 filtered</summary>

- [line
133](https://github.com/xmtp/libxmtp/blob/0972aaf951c6484cd66aee375a5c8819c1079685/xmtp_db/src/encrypted_store/refresh_state.rs#L133):
rows_to_global_cursor_map performs unchecked signed-to-unsigned integer
casts that can silently wrap negative database values, yielding
incorrect OriginatorId and SequenceId. Specifically, `originator_id as
u32` and `sequence_id.unwrap_or(0) as u64` will wrap if `originator_id`
or `sequence_id` are negative, producing large unsigned values instead
of rejecting invalid input. This can corrupt the resulting
`GlobalCursor` (e.g., mapping to the wrong originator or an enormous
sequence), leading to incorrect cursor computations downstream. Validate
non-negativity before casting, or return an error on invalid rows. <b>[
Previously rejected ]</b>
- [line
706](https://github.com/xmtp/libxmtp/blob/0972aaf951c6484cd66aee375a5c8819c1079685/xmtp_db/src/encrypted_store/refresh_state.rs#L706):
latest_cursor_combined may generate invalid SQL when `entities` contains
no kinds that map to `group_messages` (i.e., neither
`EntityKind::ApplicationMessage` nor `EntityKind::CommitMessage`). In
that case `group_message_kinds` becomes an empty Vec, producing `kind IN
()` in the constructed SQL, which is a syntax error in SQLite. This is
reachable if callers pass, e.g., only `EntityKind::Welcome`. Ensure you
guard against an empty `group_message_kinds` (e.g., omit the
`group_messages` branch or use a false predicate) before constructing
the placeholders. <b>[ Out of scope ]</b>
</details>

<details>
<summary>xmtp_mls/src/cursor_store.rs — 0 comments posted, 4 evaluated,
4 filtered</summary>

- [line
70](https://github.com/xmtp/libxmtp/blob/0972aaf951c6484cd66aee375a5c8819c1079685/xmtp_mls/src/cursor_store.rs#L70):
`sid` from the database is cast to `u64` with `as` without validating
non-negativity. If `sid` is negative (e.g., due to DB corruption or
unexpected input), it will wrap to a very large `u64`, corrupting the
cursor and causing incorrect behavior (e.g., dominance/missing
comparisons). Validate `sid >= 0` and return an error or clamp, or
change the DB/API to return an unsigned type. <b>[ Previously rejected
]</b>
- [line
101](https://github.com/xmtp/libxmtp/blob/0972aaf951c6484cd66aee375a5c8819c1079685/xmtp_mls/src/cursor_store.rs#L101):
`sid` is cast to `u64` without validating non-negativity in
`latest_per_originator` for identity updates, which can wrap negatives
to huge values and corrupt cursor semantics. Validate or use an unsigned
type. <b>[ Previously rejected ]</b>
- [line
163](https://github.com/xmtp/libxmtp/blob/0972aaf951c6484cd66aee375a5c8819c1079685/xmtp_mls/src/cursor_store.rs#L163):
Potential integer wrap: `sid` from
`get_latest_sequence_id_for_inbox(...)` is cast with `as u64` without
validation. If `sid` is negative, the cast will wrap to a very large
`u64`, producing an incorrect `GlobalCursor` value and potentially
causing downstream logic to skip messages. Validate that `sid >= 0` and
either clamp, normalize, or return an error before inserting into the
`GlobalCursor`. Affected line: `map.insert(Originators::INBOX_LOG, sid
as u64);`. <b>[ Previously rejected ]</b>
- [line
163](https://github.com/xmtp/libxmtp/blob/0972aaf951c6484cd66aee375a5c8819c1079685/xmtp_mls/src/cursor_store.rs#L163):
`sid` is cast to `u64` without validation in `latest_for_topics` for
`IdentityUpdatesV1`, which can wrap negative SIDs and produce incorrect
`GlobalCursor` values. Validate `sid` or return an error if negative.
<b>[ Previously rejected ]</b>
</details>


</details><!-- Macroscope's review summary ends here -->

<!-- macroscope-ui-refresh -->
<!-- Macroscope's pull request summary ends 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