Serialized Ledger Entries
How the XRP Ledger represents its state: Serialized Ledger Entries (SLEs), the structured objects that encode accounts, offers, trust lines, escrows, and other persistent ledger data. Learn how SLEs are defined, identified, serialized, accessed, and modified inside `rippled`, and how they connect transactions and protocol logic to the ledger’s underlying storage.
What you'll learn
- * Explain what a Serialized Ledger Entry (SLE) is and its role in representing the current state of the XRP Ledger.
- * Understand the structure of an SLE: ledger entry types, fields, flags, and serialized values.
- * Explain how SLEs are uniquely identified using ledger keys and how those keys are derived.
- * Navigate the `rippled` codebase to see how SLE types and fields are defined and accessed.
- * Understand how transaction processing reads, creates, modifies, and deletes ledger entries through ledger views.
- * Connect SLEs to the underlying SHAMap and NodeStore layers that ultimately persist ledger state.
- * Trace the lifecycle of a ledger object from transaction execution to its serialized representation in the ledger.
Serialized Ledger Entries
Introduction
≈90 min including exercises · Advanced · builds on State-management foundations and SHAMap
The SHAMap module explained how a tree commits to ledger state. Now let's open one of its state leaves: what do the bytes inside actually represent?
They represent a ledger entry: an account, a trust line, an offer, an escrow, or another piece of persistent protocol state. In rippled, the central C++ representation is STLedgerEntry, usually called SLE.
This module follows one AccountRoot from its field definitions to its binary representation and then into the state SHAMap. By the end, you should be able to:
- Distinguish an entry's key, contents, and leaf hash.
- Find the schema and serialized field definitions for an entry type.
- Explain how canonical serialization produces deterministic bytes.
- Read and modify entries through rippled's ledger-view interfaces.
- Trace a state change into both the state tree and transaction metadata.
What Is a Serialized Ledger Entry?
In brief: one typed object in ledger state, represented in memory by an SLE and in the state tree by serialized bytes plus a key.
The state SHAMap is a key-value structure. Its keys identify ledger entries; its item payloads hold their serialized fields.
State SHAMap
└── Account-state leaf
└── SHAMapItem
key: 256-bit ledger entry ID
bytes: serialized fields of one ledger entry
When application code needs to inspect these bytes, rippled deserializes them into an SLE. The object exposes typed operations such as reading an account ID or updating a sequence number.
“Account state” is the SHAMap leaf category for all ledger state entries. It includes much more than accounts:
| Entry type | State it represents |
|---|---|
AccountRoot |
An account's XRP balance, sequence, and account settings |
RippleState |
A trust line between two accounts for a currency |
Offer |
An outstanding offer to exchange assets |
Escrow |
XRP held subject to release conditions |
DirectoryNode |
A page of references to other ledger entries |
Amendments |
Network amendment state |
One account can be associated with many ledger entries. Its trust lines and offers are separate objects, with their own keys; they are not all embedded inside its AccountRoot.
An SLE also differs from a transaction. A transaction requests an operation; ledger entries represent the state on which it operates. Transactions use STTx, and transaction metadata describes the resulting entry changes.
Why This Representation Exists
In brief: protocol state needs a shared schema, deterministic bytes, and stable identifiers.
Deterministic representation. Servers must serialize the same protocol fields and values into the same bytes. Otherwise, they could agree on an account's balance yet compute different state-tree roots because their encodings differ. Canonical serialization removes representational differences; deterministic transaction execution is also necessary to make their state agree.
Shared field machinery. Accounts, offers, and escrows have different fields, but they share infrastructure for field access, binary encoding, JSON output, and structural validation. Each entry type supplies a template instead of reimplementing those mechanisms.
Stable lookup. Application code can derive an entry's key from its identifying inputs. Changing an account's balance does not move its AccountRoot to a new position in the state tree.
Compact storage and exchange. The binary representation uses numeric field identifiers and type-specific encodings. It does not repeat JSON field names or store account addresses as Base58 strings.
Key idea. The SLE layer defines the structured contents of a state item. The SHAMap layer commits to those contents and their position in the tree.
The C++ Object
In brief: STLedgerEntry adds an entry key and ledger-entry type to the general serialized-object infrastructure.
From STLedgerEntry.h, abridged:
class STLedgerEntry final : public STObject, public CountedObject<STLedgerEntry>
{
uint256 key_;
LedgerEntryType type_;
public:
explicit STLedgerEntry(Keylet const& k);
STLedgerEntry(SerialIter& sit, uint256 const& index);
uint256 const& key() const;
LedgerEntryType getType() const;
};
using SLE = STLedgerEntry;
The fields such as Balance and Sequence belong to the inherited STObject representation. They are not separate C++ data members declared on STLedgerEntry.
The relevant layers are:
| Component | Responsibility |
|---|---|
SField |
Describes a field's identity, serialized type, and other field properties |
STUInt32, STAmount, STAccount, etc. |
Hold and serialize typed values |
STObject |
Holds fields and implements object serialization and template handling |
SOTemplate |
Describes the permitted fields and their presence rules |
LedgerFormats |
Associates ledger-entry types with their templates |
STLedgerEntry / SLE |
Combines an STObject with ledger-entry identity and type |
Do not confuse three different uses of “type”:
ltACCOUNT_ROOTidentifies a kind of ledger entry.STI_UINT32identifies a field's serialized value type.STI_LEDGERENTRYis the high-level serialized type reported bySTLedgerEntry::getSType().
The LedgerEntryType field inside an AccountRoot carries the numeric value 0x0061, encoded as a UInt16. Its JSON representation uses the readable name AccountRoot.
Schemas: Which Fields Belong Here?
In brief: a ledger entry is constrained by a template; it is not an arbitrary dictionary.
Entry definitions live in ledger_entries.macro. These are selected lines from the AccountRoot definition, with intervening optional fields omitted:
LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfAccount, SoeRequired},
{sfSequence, SoeRequired},
{sfBalance, SoeRequired},
{sfOwnerCount, SoeRequired},
{sfPreviousTxnID, SoeRequired},
{sfPreviousTxnLgrSeq, SoeRequired},
// ...
{sfDomain, SoeOptional},
// ...
{sfMintedNFTokens, SoeDefault},
{sfBurnedNFTokens, SoeDefault},
// ...
}))
LedgerFormats.cpp adds common fields. At this revision, these are required LedgerEntryType and Flags, and optional LedgerIndex and Sponsor. This shared template is not a statement that every transaction may set every common field on every entry type.
Required, Optional, and Default
The three styles in SOTemplate.h have distinct meanings:
| Style | Meaning when applying a template to parsed fields |
|---|---|
SoeRequired |
The field must be present. |
SoeOptional |
The field may be absent; an explicitly present default value is allowed. |
SoeDefault |
The field may be absent; if present, its value must be non-default. |
“Default” depends on the value type. For a UInt32, it is zero. An absent MintedNFTokens field represents the default; explicitly encoding MintedNFTokens = 0 fails the SoeDefault check when the template is applied.
An absent optional field and a present field containing zero are different representations. Do not assume a zero-valued getter proves a field is present; use isFieldPresent when presence matters.
Construction Is Not Full Validation
Creating SLE(keylet) installs its template, creates default-valued required fields, and sets LedgerEntryType. It does not create a funded, protocol-valid account. The caller still has to populate meaningful values and perform the appropriate ledger operations.
When deserializing, STLedgerEntry::setSLEType finds the format from LedgerEntryType and applies its template. Unknown types, missing required fields, and disallowed non-discardable fields cause errors. SoeDefault rules are checked here too.
These are structural checks. Authorization, reserve requirements, amendment rules, valid relationships between entries, and other transaction semantics require additional validation in transaction processing and invariant checks.
Identity: Key, Contents, and Hash
In brief: the entry key identifies an object; the leaf hash commits to that object's contents at a particular state.
For an AccountRoot, the key is derived as:
accountKey = SHA512Half(
UInt16BE(0x0061) || AccountID
)
AccountID: the decoded 20-byte identifier
UInt16BE: a two-byte integer in big-endian order
||: byte concatenation
SHA512Half means the first 256 bits of SHA-512. Here 0x0061 is the account namespace used in key derivation. Namespace identifiers and LedgerEntryType values serve separate purposes even when, as here, their numeric values match.
Use keylet::account rather than hand-implementing this formula in application code. Other entry types have their own derivation rules. A trust-line key, for example, uses both accounts in a defined order and the currency.
A Keylet combines a key with an expected entry type. A typed lookup can therefore check that the object at that key has the expected type.
The state leaf hash is a different calculation:
leafHash = SHA512Half(
'MLN\0' || serializedEntry || entryKey
)
This is the formula used by SHAMapAccountStateLeafNode. The hash prefix is four bytes, whereas the account key namespace above is two bytes.
Change an AccountRoot balance:
AccountID stays the same
entry key stays the same
serialized contents change
state leaf hash changes, under the hash's collision-resistance assumption
ancestor hashes change up to the state root
The state root commits to state. It is itself one component of the ledger header; it is not the complete ledger hash.
What About index in JSON?
STLedgerEntry::getJson() adds lowercase index from key_. The deserializing constructor likewise receives the key separately from the serialized fields.
Therefore, do not model the SHAMap item's key as a mandatory field inside its payload. There is also a separately defined optional sfLedgerIndex; that does not make it interchangeable with key_ or the JSON field added by getJson().
Also distinguish an entry ID from the ledger sequence number. API ledger_index identifies a ledger version, while entry index identifies an object. See the ledger-entry common fields reference.
Serialization: Fields Become Bytes
In brief: present binary fields are sorted by numeric field identity, then each writes its identifier and encoded value.
The object is not serialized by dumping C++ memory. Pointers, padding, reference counts, and machine-native object layout are not protocol data.
STObject::add selects the fields to serialize, sorts them by field code, and writes each field's identifier followed by its value. Ordinary SLE serialization uses the all-fields path, subject to the field's binary inclusion rules.
Canonical Order
Fields are ordered first by serialized type code, then by the field number within that type. This is neither alphabetical order nor the order in which setters were called. See sfields.macro for the definitions.
For a minimal AccountRoot:
| Field | Type code | Field number | Position |
|---|---|---|---|
LedgerEntryType |
1 (UInt16) |
1 | 1 |
Flags |
2 (UInt32) |
2 | 2 |
Sequence |
2 (UInt32) |
4 | 3 |
PreviousTxnLgrSeq |
2 (UInt32) |
5 | 4 |
OwnerCount |
2 (UInt32) |
13 | 5 |
PreviousTxnID |
5 (UInt256) |
5 | 6 |
Balance |
6 (Amount) |
2 | 7 |
Account |
8 (AccountID) |
1 | 8 |
Notice that Account appears first in the entry template but last in this binary representation.
Field Identifiers
When both the type code and field number are below 16, they fit in one byte:
field ID = (type code << 4) | field number
LedgerEntryType: (1 << 4) | 1 = 0x11
Sequence: (2 << 4) | 4 = 0x24
OwnerCount: (2 << 4) | 13 = 0x2D
For larger codes, a zero nibble signals an additional byte. Serializer::addFieldID emits identifiers of one, two, or three bytes. The compact header is not a general value-length header: value size is determined by the serialized type and its encoding rules.
Values
Fixed-width integers use big-endian encoding. Variable-length fields include a protocol length prefix. An ordinary populated Account field writes a length prefix followed by its 20 decoded bytes; it does not write the textual address. See STAccount::add.
STAmount has its own encoding. For a positive XRP amount, the eight-byte value contains the drop count with the positive flag set. It is not an ordinary untagged UInt64, and amounts for other asset kinds have different layouts. See STAmount::add.
Nested objects serialize their fields in canonical order. Arrays retain their element order. The enclosing serializer writes the appropriate end marker for a nested object or array; a top-level SLE payload does not acquire an outer object field header and terminator. The binary-format reference explains these encoding rules in more detail.
Encoding and Parsing Are Different Contracts
Canonical output does not mean every parsing entry point rejects noncanonical input order. At this revision, STObject::set has a requireCanonicalOrder argument that defaults to false; the SLE deserializing constructor calls set(sit) without enabling it. The parser still checks for duplicate serialized fields, and template validation follows.
When reviewing a parsing path, inspect its actual options and surrounding checks rather than assuming that “deserialized successfully” means “verified canonical input.”
Worked Example: An 87-Byte AccountRoot
In brief: a complete minimal field set makes the encoding tangible.
Use these deliberately synthetic values. This is a serialization fixture, not an account retrieved from a network or evidence of a valid ledger history:
Account: 20 bytes of 0x11
Balance: 100,000,000 drops = 100 XRP
Sequence: 1
OwnerCount: 0
Flags: 0
PreviousTxnLgrSeq: 100
PreviousTxnID: 32 bytes of 0x22
LedgerEntryType: AccountRoot (0x0061)
Its serialized fields are:
| Field | Header and value bytes | Total bytes |
|---|---|---|
LedgerEntryType |
11 00 61 |
3 |
Flags |
22 00 00 00 00 |
5 |
Sequence |
24 00 00 00 01 |
5 |
PreviousTxnLgrSeq |
25 00 00 00 64 |
5 |
OwnerCount |
2D 00 00 00 00 |
5 |
PreviousTxnID |
55 followed by 32 bytes of 22 |
33 |
Balance |
62 40 00 00 00 05 F5 E1 00 |
9 |
Account |
81 14 followed by 20 bytes of 11 |
22 |
| Total payload | Concatenate the rows in order | 87 |
The balance's 0x4000000000000000 flag marks it nonnegative; 0x05F5E100 is 100,000,000. The 0x14 after the Account field header is its 20-byte length.
The 87 bytes exclude the SHAMap item's key and any SHAMap node framing. Here is a runnable Python reproduction of this specific fixture, using the encoding rules above. It is not a general XRPL codec:
from hashlib import sha512
def sha512_half(data):
return sha512(data).digest()[:32]
account = bytes.fromhex("11" * 20)
payload = bytes.fromhex(
"110061" # LedgerEntryType
"2200000000" # Flags
"2400000001" # Sequence
"2500000064" # PreviousTxnLgrSeq
"2d00000000" # OwnerCount
"55" + "22" * 32 # PreviousTxnID
+ "624000000005f5e100" # Balance
+ "8114" + account.hex() # Account
)
key = sha512_half(bytes.fromhex("0061") + account)
leaf_hash = sha512_half(b"MLN\0" + payload + key)
assert len(payload) == 87
assert key.hex() == (
"904d7d725f60af746ff6163f6978800cb684b4f59248920ffece3fc3ef1a9ea0"
)
assert leaf_hash.hex() == (
"610574742dde8248f08dd59dcb67a97c5584fd98ec2de5f83df25f52ca421633"
)
This shows why three operations must remain distinct: deriving the account key, serializing the fields, and hashing the leaf.
Reading and Changing Ledger Entries
In brief: transaction code accesses state through ledger views, which record changes before those changes reach the underlying ledger.
Read-Only Access
A ReadView exposes read(Keylet const&), returning std::shared_ptr<SLE const>.
Illustrative code inside a function with a ReadView const& view and AccountID const& account:
auto const sle = view.read(keylet::account(account));
if (!sle)
return; // No matching AccountRoot in this view.
auto const balance = sle->getFieldAmount(sfBalance);
auto const sequence = sle->getFieldU32(sfSequence);
For a concrete ledger, the underlying path looks up the SHAMap item and constructs an SLE using both the item's bytes and its key. View layers and caches can serve the read without repeating that entire path on every call.
Mutable Access
ApplyView provides the transaction-facing operations:
| Operation | Purpose |
|---|---|
peek(keylet) |
Obtain a mutable entry associated with this view, if it exists and matches. |
update(sle) |
Record a modification to an existing entry. |
insert(sle) |
Record a new entry. |
erase(sle) |
Record removal of an existing entry. |
An illustrative field update, inside transaction application code that has already validated the change:
auto sle = view.peek(keylet::account(account));
if (!sle)
return; // Real transaction code supplies the appropriate result.
sle->setFieldU32(sfSequence, newSequence);
view.update(sle);
newSequence here is a value computed under the transaction's rules, not permission to change sequence numbers arbitrarily.
The update call matters: changing a C++ object does not by itself complete a ledger mutation. Follow the view contract even if the implementation currently holds a pointer to that same object. Similarly, after erasing an entry, treat it as removed rather than continuing to mutate it through a retained pointer.
Views allow transaction application to stage changes. Whether staged effects are committed, discarded, or replaced by a fee-only result depends on the transaction result and application path. Avoid the blanket rule “every failed transaction makes no state changes”: some included failures still charge a fee.
From View Changes to SHAMap Items
When changes reach the concrete Ledger, insertion and replacement serialize the SLE and create a SHAMap item using its key. Erasure removes the item by key.
Transaction application
→ read / peek entries
→ validate and compute field changes
→ update / insert / erase in the view
→ apply the staged result to the target view or ledger
→ serialize inserted or replaced SLEs
→ change keyed items in the state SHAMap
→ recompute affected hashes as the tree is finalized/hashed
The SHAMap's copy-on-write machinery protects shared tree snapshots. The ledger views manage application-level entry changes. These are related mechanisms at different layers.
Also separate updating the state tree from persisting nodes to NodeStore: an SLE setter is not a database write, and an SLE is not itself a NodeStore record.
Entries and Transaction Metadata
In brief: the state tree records the resulting objects; metadata describes how a transaction affected them.
A payment may affect multiple entries. Its metadata identifies the changed entries under AffectedNodes, using wrappers such as:
CreatedNode: an entry was created.ModifiedNode: an existing entry changed.DeletedNode: an entry was removed.
Each affected node identifies the entry by LedgerIndex and LedgerEntryType. Depending on the change and the field's metadata policy, it includes NewFields, FinalFields, or PreviousFields.
Metadata is not simply a complete copy of every entry before and after the transaction. In particular, PreviousFields records selected previous values of changed fields. Field metadata flags influence what is emitted. Trace this in ApplyStateTable.cpp and the field definitions in sfields.macro.
For a closed ledger, transaction metadata is stored alongside its transaction in the transaction SHAMap. It is not an additional SLE in the state SHAMap. TxMeta.cpp shows how affected-node metadata receives the entry key as LedgerIndex.
For entry types that support transaction threading, PreviousTxnID and PreviousTxnLgrSeq link the current entry to the transaction that most recently changed it and the ledger containing that transaction. They do not embed the previous version of the entry. Eligibility for threading can depend on amendment rules; inspect STLedgerEntry::isThreadedType rather than assuming every entry is treated identically.
State SHAMap: What does this entry contain now?
Transaction metadata: What did this transaction create, change, or delete?
Practical Exercises
≈30–40 min · use the pinned source revision or record the revision you investigate.
1. Follow an AccountRoot Through the Source
Find its definition in ledger_entries.macro, then locate each required field in sfields.macro.
Explain why Account comes after Balance in the serialized payload, even though it appears before Balance in the template. Identify which required fields come from the common template.
Expected result: ordering comes from numeric serialized field identity; LedgerEntryType and Flags are the required common fields at this revision.
2. Change a Value Without Changing Identity
Run the Python fixture. Change the balance from 100 XRP to 99 XRP, using (0x4000000000000000 | 99_000_000).to_bytes(8, "big") for its value bytes. Recompute the key and leaf hash, replacing the original leaf-hash assertion with a comparison.
Expected result: the key is unchanged; the leaf hash differs; the payload remains 87 bytes.
3. Test Template Behavior in rippled
Using the patterns in STObject_test.cpp, build and serialize the fixture in C++, then deserialize it with its key. Compare the serialized bytes to the Python fixture.
Next, construct parsed-object variants before applying the SLE template:
- Omit required
Sequence. - Include
MintedNFTokensexplicitly as zero. - Include
MintedNFTokensas one.
Expected result: the first two variants fail template validation; the third passes those structural checks. This does not establish that the third describes a valid account history.
Use parsed fields or an untemplated STObject for the missing-field case: constructing a fresh typed SLE automatically creates its required fields.
4. Trace One Real Mutation
Choose a transaction implementation that uses peek and update. Record the entry's keylet, changed fields, and where the view receives the update. Follow the change into ApplyStateTable, then into the concrete ledger's replacement operation.
Expected result: you can point to the boundary between changing fields, recording a view modification, and serializing the replacement state item.
Appendix: Codebase Navigation Guide
In brief: read from the object model outward to serialization and ledger integration.
| Start here | What to inspect |
|---|---|
STLedgerEntry.h and STLedgerEntry.cpp |
Identity, constructors, template application, JSON, threading |
ledger_entries.macro |
Entry types and their field templates |
LedgerFormats.cpp |
Registry construction and common fields |
SField.h and sfields.macro |
Field identifiers, types, and metadata policies |
STObject.cpp |
Parsing, presence, template validation, canonical output |
Serializer.cpp |
Field headers, byte writing, length prefixes |
Indexes.cpp |
Key derivation and keylet helpers |
ReadView.h and ApplyView.h |
Read and mutation contracts |
ApplyStateTable.cpp |
Staged changes and metadata generation |
Ledger.cpp |
Conversion between SLEs and state SHAMap items |
For encoding tests, also read Serializer_test.cpp. For API behavior, inspect LedgerEntry_test.cpp.
To Remember
- An SLE is rippled's typed in-memory representation of one ledger state entry.
- Its template defines field presence rules; transaction processing supplies the semantic rules.
- The entry key identifies the object. Canonical serialization encodes its fields. The state leaf hash commits to both.
readgives const access; mutable view operations explicitly record insertions, modifications, and deletions.- The state SHAMap stores the result, while transaction metadata describes the affected entries.
Next up. We’ve seen how ledger entries become serialized bytes in the SHAMap. Next, we’ll follow that state into persistent storage: how rippled’s NodeStore and database backends store and retrieve tree nodes, and how database operations support the ledger’s lifecycle.
Trace an SLE Through rippled
Format: Group investigation
Duration: 45–60 minutes, plus approximately 3 minutes per group for presentations
Level: Advanced — XRPL Core Developer Bootcamp
Repository: XRPLF/rippled
Objective
Each group investigates a different Serialized Ledger Entry (SLE) type: representation, access, modification, serialization, deserialization, and storage in the state SHAMap. Compare your findings to identify shared infrastructure and entry-specific behavior.
Your task is to navigate source code and observe execution. Keep transaction rules and test assertions intact. Do not manufacture an artificial lifecycle or change fields outside the protocol just to produce a log.
Learning objectives
By the end, you should be able to:
- Locate an entry's schema, identity, key derivation, and transaction implementations.
- Trace construction, reading, and deserialization, including paths that reuse cached objects.
- Explain how mutable ledger views record changes.
- Identify the boundary between typed C++ fields and serialized bytes.
- Follow insertion, replacement, or removal in the state SHAMap and investigate the resulting hashes.
- Distinguish in-memory SLEs, staged changes, tree updates, and persistent storage.
Prerequisites and current revision
Complete the SHAMap and SLE presentations and prepare a working Debug development build with unit tests using the Build & run your own xrpld node prerequisite. Be comfortable with C++ navigation, breakpoints or focused logging, and rebuilding your configured target.
Use the latest rippled version prepared for the bootcamp session. An older version number in the prerequisite is not the version requirement for this exercise. At the 13 October 2026 check, the latest stable release is 3.4.1. The source links below are fixed to its public release commit 00e640751460c137636fdb6dae6b4280de355d81 so line references remain reproducible.
When the session moves to a newer revision, the instructor should rerun the scenarios and update these source references. Record your own checkout revision, executable version, build configuration, and actual commands in the report; investigate any differences from linked sources.
Build and test workflow
Reuse the Debug build already configured in the prerequisite or supplied by the instructor. In the prerequisite's existing build directory, its documented rebuild command is:
cmake --build . --parallel 10
Use your configured build directory, target, and parallelism; rebuild after instrumenting. This is the course's documented rebuild command. Validation below used a packaged executable, not a fresh source build.
Run only your group's suite using your actual executable path. The examples assume the prerequisite's ./xrpld layout; some configurations use ./Debug/xrpld. The suite argument is the same:
./xrpld --version
./xrpld --unittest=AccountSet
Replace AccountSet with your group's suite. Fixtures provide deterministic accounts without a faucet or running node. Suites execute additional scenarios: filter logs by entry key, case, and operation. The commands select suites, not individual C++ test methods.
Scenario validation: the packaged xrpld 3.4.1 executable successfully ran AccountSet,TrustSet,Offer,Escrow,Check,PayChan (10 suites, 362 cases, 51,408 tests, 0 failures) and TrustAndBalance (1 suite, 24 cases, 1,269 tests, 0 failures). Offer also selects related offer suites. The packaged binary's build commit differs from the public release commit; selected source files were checked for correspondence. This is a runtime baseline, not an instrumented trace or fresh source build. Recheck the fixtures in the cohort's Debug build before the session.
Group setup
The instructor chooses 1–6 groups and assigns a different entry type to each group. With fewer than six groups, select a subset. Organize participants separately from the entry-type assignment and have each group record its number, members, and assigned type.
Each group follows one assignment below and completes Parts 1–4. Common instructions apply to every group; there is no requirement to complete the other five scenarios.
| Entry type | Controlled scenario | Unit-test suite |
|---|---|---|
AccountRoot |
Fund an account, then set its Domain with AccountSet |
AccountSet |
RippleState |
Create a USD trust line, then change its nonzero limit | TrustAndBalance |
Offer |
Create an offer that rests on the book, then cancel it | OfferBaseUtil |
Escrow |
Create an escrow, then finish or cancel it using the fixture's controlled time | Escrow |
Check |
Create a check, then cancel it | Check |
PayChannel |
Create a payment channel, then add funds | PayChan |
AccountRoot assignment
Use AccountSet_test::testDomain. The fixture funds alice with XRP and then submits an AccountSet setting Domain to example.com. Trace the funding-created entry and the first successful Domain-setting transaction. Later transactions clear the Domain or test its size limits; keep those separate from your main trace.
./xrpld --unittest=AccountSet
Optional navigation hint: follow the fixture's noop(alice) helper to its transaction type, then explore AccountSet.cpp and the account-creation branch in Payment.cpp. Which changes belong to settings, and which belong to fees or sequence handling?
RippleState assignment
Use TrustAndBalance_test::testCreditLimit. Reuse gateway (gw), alice, and the fixture's funding/ledger-close setup. Focus on the first USD trust line from Alice to the gateway with limit 800, followed by a TrustSet changing that limit to 700. Preserve the existing assertions that check the line after each transaction. The later negative-limit and zero-limit cases are outside this group's required trace.
./xrpld --unittest=TrustAndBalance
Optional navigation hint: inspect the trust transaction helper, TrustSet.cpp, and Indexes.cpp. Determine which endpoint is low/high from the actual account IDs. Which limit field changes, and how do you establish that you are still following the same entry?
Offer assignment
Use OfferBaseUtil_test::testCanceledOffer. Reuse gateway (gw), alice, the USD trust line, funding, and initial offers. Focus on the final sequence using offer4Seq: create the offer with XRP(222) and usd(111), close the ledger, verify isOffer(...), then cancel that sequence and verify its absence.
./xrpld --unittest=OfferBaseUtil
The fixture has no opposing offers for this scenario. Keep the offer-existence assertions: transaction success alone is insufficient evidence that an offer rested on the book or that the cancellation removed your target.
Optional navigation hint: start at the DEX transactors (OfferCreate, OfferCancel) and OfferHelpers.cpp. Follow creation and deletion naturally; an in-place modification is not required.
Escrow assignment
Use Escrow_test::testTiming. Reuse funded alice and bob and the fixture's XRP(1000) escrow. The instructor selects one existing successful branch: Finish Only, or Cancel Only. Keep its exact FinishAfter/CancelAfter values, sequence references, and env.close(...) time advances. These are harness-controlled conditions; do not substitute wall-clock sleeping.
./xrpld --unittest=Escrow
Optional navigation hint: follow the escrow transactors (EscrowCreate, EscrowFinish, EscrowCancel). If investigating cryptographic conditions as an extension, reuse the matching condition/fulfillment pairs from testEscrowConditions in the same test file and escrow.h; do not invent a pair.
Check assignment
Use Check_test::testCancelValid. Reuse the funded gateway, alice, bob, and zoe fixtures. Choose an existing successful Alice-to-Bob creation and cancellation by Alice or Bob, preserving its SendMax, sequence, and assertions. Focus on the unexpired authorized-cancellation path; later third-party and expiration cases are separate observations.
./xrpld --unittest=Check
Optional navigation hint: explore the check transactors (CheckCreate, CheckCancel). What must be true for cancellation to succeed, and which ledger entries are affected besides the check?
PayChannel assignment
Use PayChan_test::testSimple. Reuse alice, bob, Alice's fixture public key, the 100-second settle delay, and the channel key captured from the creation sequence. Trace creation with XRP(1000), then PaymentChannelFund adding XRP(1000) to that same channel. Preserve the assertions around Amount and Balance.
./xrpld --unittest=PayChan
Optional navigation hint: start at the payment-channel transactors (PaymentChannelCreate, PaymentChannelFund). Which fields distinguish funding from a claim? Which values identify the channel across both transactions?
Part 1 — Understand your entry
Locate your assigned entry's schema and transaction implementation. Investigate:
- What protocol state does this entry represent?
- Which fields are required or optional? Which fields matter to your scenario?
- Where is the schema defined, and how does an SLE acquire its ledger-entry type?
- How is the 256-bit ledger-entry key computed? Is it the same as an account address?
- Which transactions can create, modify, or delete this type?
Expected result: a brief description of the entry, its important fields, its key derivation, and its supported lifecycle operations.
Optional navigation hint: use ledger_entries.macro for the schema, then follow the relevant keylet through Indexes.h and Indexes.cpp. Use your assigned transaction's call sites to keep the search focused.
Part 2 — Trace the entry in rippled
Run the assigned scenario with focused logs or breakpoints. Record the target key and selected field values so that incidental AccountRoot, directory, or other entry changes do not become confused with your assigned object.
Find and observe, where applicable:
- Creation: where is the SLE constructed and given its identity?
- Reading: how does application code retrieve it? Compare read-only and mutable access.
- Deserialization: where can serialized bytes become a typed SLE?
- Modification: which fields change, and where is the change recorded in a ledger view?
- Serialization: where are the fields converted to binary bytes? Record the payload size.
- SHAMap update: where is the keyed item inserted, replaced, or removed? Follow the effect on tree hashes.
Not every operation must occur. An Offer or Check may be created and deleted; a cached read may not decode bytes. If a stage is absent, investigate the reason. An isolated serialization/deserialization test is allowed, but label it separately from the transaction trace.
Use these C++ symbols as navigation starting points; follow the concrete implementation used by your scenario:
STLedgerEntry
SerialIter
STObject
Serializer
ReadView::read(...)
ApplyView::peek(...)
ApplyView::insert(...)
ApplyView::update(...)
ApplyView::erase(...)
SHAMapItem
Shared questions to investigate
- What does a read-only view return? How does
peek()differ fromread()? - What information does
SerialIterprovide, and how are fields reconstructed into typed values? - How does
STLedgerEntrydetermine its entry type? Why might a read skip decoding? - Does a field setter immediately change the SHAMap? What does the view's update operation record?
- Why can the key remain unchanged when the contents change? Is that key an ordinary field in the serialized payload?
- Why must serialization be deterministic? What changes for your operation: key, payload, leaf hash, tree structure, or several of these?
- When are the affected hashes recomputed? What causes the state root to change?
- Does a SHAMap update immediately mean a disk write? Which extra steps connect the tree to persistent storage?
Use source references and observations to support your answers. Implementing the hashing algorithm is not required.
Part 3 — Reconstruct the lifecycle
Draw the actual execution path your group followed. Include the relevant C++ functions/classes and label these boundaries:
- The typed in-memory SLE.
- Staged insertions, modifications, or deletions in ledger views.
- The serialized binary payload.
- The SHAMap item associated with the ledger-entry key.
- Changes to the state SHAMap and its hashes.
Mark each transition observed in logs/breakpoints, established by source inspection, or not observed, with an explanation. Include caches and intermediate views when they appear in your trace. Creation, modification, deletion, and loading do not have to form one continuous path.
Recording template (not executed evidence):
<order> | <entry key> | <C++ function> | <source file:line>
| <selected fields / payload size / tree operation>
| <observed / source inspection / not observed and why>
Part 4 — Prepare group findings
Prepare an approximately 3-minute technical presentation covering:
- The entry: what does it represent, and what identifies it?
- Its lifecycle: which transactions create, modify, or delete it?
- The C++ trace: which functions did you instrument, and what did the logs reveal?
- Serialization: where do fields become bytes, and when might bytes become an object?
- SHAMap integration: what changed in the state tree?
- Unexpected findings: were operations skipped, was the entry deleted instead of updated, or did other entry types change in the same transaction?
Deliverables
Each group submits one shared report or report link containing:
- The group number, members, assigned entry type, checkout revision, executable version, and actual build/test commands.
- A lifecycle diagram with relevant C++ functions and source references.
- Selected logs or breakpoint observations demonstrating significant operations, in chronological order.
- A short explanation of entry identity, binary representation, ledger views, SHAMap integration, caching, and persistence.
- A summary of stages observed, established by inspection, or not observed and why.
Keep excerpts focused; do not upload thousands of lines of raw logs. Use the existing mentor-reviewed report assignment. Academy records submissions and completion per learner: a representative's submission does not automatically credit teammates. If every member needs completion recorded, each can submit the same shared report link with their name and group.
Cross-group comparison
After the presentations, compare the evidence across groups. Discuss:
What was different? Entry schemas, key derivation, transaction rules, supported lifecycle operations, and the specific fields changed.
What was shared? SLE representation, typed field infrastructure, serialization machinery, ledger views, and SHAMap integration.
Support comparisons with code or observations. Investigate whether caching, intermediate views, or scenario differences explain different paths through shared infrastructure.
Optional source navigation
Follow components relevant to your trace and record differences when the cohort upgrades.
| Component | Investigation starting point |
|---|---|
| SLE identity and decoding | STLedgerEntry.cpp |
| Typed fields and serialization | STObject.cpp |
| Binary writing and reading | Serializer.h (including SerialIter) |
| Ledger access contracts | ReadView.h / ApplyView.h |
| Intermediate views and staged actions | ApplyStateTable.cpp |
| Concrete ledger reads and raw updates | Ledger.cpp |
| Keyed serialized item | SHAMapItem.h |
| State tree operations and flushing | SHAMap.cpp |
| Leaf and inner-node hashes | SHAMapAccountStateLeafNode.h / SHAMapInnerNode.cpp |
Completion criteria
The investigation is complete when your group can explain its entry's identity and actual lifecycle with code references and selected observations, locate the structured-fields/serialized-bytes boundary, and distinguish staged changes, SHAMap changes, and persistent storage.
Explain absent operations instead of inventing a trace. You are not expected to observe every lifecycle stage in a single transaction or demonstrate creation, modification, and deletion for every type.
Optional extensions
- Investigate a different supported lifecycle operation for your assigned type.
- Compare the serialized payload before and after a change, where both states exist.
- Recompute a state leaf hash using the correct hash prefix, payload, and entry key.
- Compare the resulting state with the appropriate
CreatedNode,ModifiedNode, orDeletedNodein transaction metadata.
Final question
At what exact points did your entry transition between structured C++ fields and serialized bytes, which components were responsible, and which of those mechanisms were shared with the other groups?
Different entry types share technical infrastructure. Use these findings to navigate unfamiliar types.
Resources
Assignments
0 of 1 complete