Skip to content

Conversation

@graydon
Copy link
Contributor

@graydon graydon commented Dec 5, 2025

This is an initial implementation of the idea described in #4971 -- what I'm now calling "ledger entry scopes"

At a high level, the idea here is to:

  • Wrap LedgerEntries in helper classes called ScopedLedgerEntry<S> where S is one of a static set of known scope-types (GLOBAL_PAR_APPLY_STATE, THREAD_PAR_APPLY_STATE, TX_PAR_APPLY_STATE and so forth).
  • Add a mix-in class LedgerEntryScope<S> that is inherited by each class we use to store "random hashtables-full-of-LedgerEntries" like ThreadParallelApplyLedgerState or GlobalParallelApplyLedgerState or such)
  • Require that when you unwrap a ScopedLedgerEntry to read or modify the contained LedgerEntry, you provide the scope you're reading or writing it in, and statically require the S parameters match.
  • Also dynamically require that a couple other parameters stored in the ScopedLedgerEntry match the scope: the ledger number and a generic "index" number which can be used to encode either the thread-cluster number or the bucket list number or similar things where there are "a bunch of similar groups of LEs that should nonetheless be kept separate".
  • Track when each of those scopes is "active" the same way the LTX thinks about active-ness: when active you can do the read/write actions directly, but when inactive all you can do is "adopt" the entries into some other scope for reading and writing. This is to help catch stale reads on entries from inactive scopes (eg. reading from the global scope directly rather than through a single method that adopts from global -> thread and then reads at the thread scope)

All told this should provide a similar level of protection from stale reads as we're getting with the LTX, but in a sort of "decentralized" way that doesn't require uniformly using an LTX for everything. Neither system provides perfect protection against stale reads -- you can hold on to an unwrapped LedgerEntry longer than you should or pass it somewhere you shouldn't -- but it at least gives us a bit more structure to lean on to prevent such errors.

Caveats

It's not wired-in to the bucket list or in-memory soroban state yet. I mean to do so but that'll come later.

I tried to be a little more explicit than the methods in the LTX: to not use operator* or operator(bool) or such, and not rely on destructors more than necessary (there's one guard object type to help with exception safety). I also named all the scope-mixin-provided methods with scope_ as a prefix, which might make it a little more verbose but I think clarity should be the main priority with this code.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new ledger entry scoping system to prevent stale reads during parallel transaction application. It wraps LedgerEntry objects in ScopedLedgerEntry<S> templates where S is a static scope type (GLOBAL, THREAD, or TX), and requires static and dynamic validation when reading or modifying entries.

  • Implements LedgerEntryScope and ScopedLedgerEntry classes with scope tracking and validation
  • Renames operation-level classes from "Op" to "Tx" prefix to better reflect transaction-level scope
  • Updates parallel apply infrastructure to use scoped entries with adoption patterns between scope levels

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/ledger/LedgerEntryScope.h Defines core scoping infrastructure including ScopedLedgerEntry, LedgerEntryScope, and DeactivateScopeGuard templates
src/ledger/LedgerEntryScope.cpp Implements scoping logic including scope validation, entry adoption, and scope ID management
src/transactions/TransactionFrameBase.h Updates type aliases and structures to use scoped entries; renames Op* types to Tx* to reflect transaction-level scope
src/transactions/ParallelApplyUtils.h Updates method signatures and class names from Op to Tx prefix; adds scope inheritance to state classes
src/transactions/ParallelApplyUtils.cpp Implements scope adoption patterns for moving entries between global, thread, and transaction scopes
src/transactions/TransactionFrame.cpp Updates transaction application to use scoped return values with proper scope IDs
src/transactions/TransactionMeta.cpp Updates meta builder to read entries through scope validation
src/transactions/InvokeHostFunctionOpFrame.cpp Updates operation frame to use renamed Tx state classes
src/transactions/RestoreFootprintOpFrame.cpp Updates operation frame to use renamed Tx state classes
src/transactions/ExtendFootprintTTLOpFrame.cpp Updates operation frame to use renamed Tx state classes and return proper scoped values
src/ledger/LedgerManagerImpl.cpp Implements proper scope deactivation patterns when launching parallel threads

@graydon graydon force-pushed the scoped-ledger-entry branch from 3dac3dd to fa9fe12 Compare December 8, 2025 19:50
Copy link
Contributor

@SirTyson SirTyson left a comment

Choose a reason for hiding this comment

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

Thank you for working on this! It looks like a fairly tricky change, but it looks like we ended up with some nice checks without being too intrusive to the callers.

Most of my comments are more like feature requests I thought of while reviewing and are probably outside of scope, but wanted to note for future followups. The one comment I do think is worth addressing is not attaching scope info to null entries, which seems like a potential footgun.

Copy link
Contributor

@SirTyson SirTyson left a comment

Choose a reason for hiding this comment

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

LGTM! Last comment is about the modify flow, which can still be hardened a bit I think, but overall it's looking very nice!

bool operator<(ScopedLedgerEntryOpt const& other) const;
};

#define SCOPE_ALIAS(SCOPE) \
Copy link
Contributor

Choose a reason for hiding this comment

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

I think a comment here describing this macro generates types like TxParApplyLedgerEntryOpt would be helpful, which also references back to the types in FOREACH_STATIC_LEDGER_ENTRY_SCOPE. Reason being is whenever I click on a generated type name, my IDE brings me to this macro, which is confusing to someone who hasn't been starting at this file for a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@graydon graydon force-pushed the scoped-ledger-entry branch from ae480ec to ccab465 Compare December 19, 2025 21:25
@graydon
Copy link
Contributor Author

graydon commented Dec 19, 2025

Ok I've addressed all review comments and added the big block comment I meant to get around to. I think this is ready.

@graydon graydon requested review from SirTyson and dmkozh December 19, 2025 22:31
SirTyson
SirTyson previously approved these changes Dec 19, 2025
Copy link
Contributor

@SirTyson SirTyson left a comment

Choose a reason for hiding this comment

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

One nitpick on the interface, but otherwise LGTM, thanks for all the back and forth!

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