34 KiB
Protocol and Serialization
The protocol layer defines XRPL's wire format, type system, and validation rules. It owns the canonical binary encoding required for signatures and consensus, the macro-driven registries for features/transactions/ledger entries/sfields/permissions, the typed object model (STBase hierarchy) that every transaction and ledger object inhabits, the cryptographic primitives, and the JSON/RPC boundary.
Layered Type System
Asset = std::variant<Issue, MPTIssue> ← unified asset identity (XRP/IOU/MPT)
Issue = (Currency, AccountID) ← XRP iff currency==zero
MPTIssue = wraps MPTID (192-bit: seq32 || account160)
Amount types (lean, runtime polymorphic via Asset):
XRPAmount = int64 drops ← integral, no asset
IOUAmount = (mantissa, exponent) ← 15-digit decimal floating point
MPTAmount = int64 ← integral, no asset
STAmount = unified wire/serialized form holding Asset + value
- holds<Issue>(), holds<MPTIssue>(), native(), integral()
- canonicalize() normalizes (mantissa, exponent) per asset rules
- Internal: mAsset + mValue(uint64) + mOffset(int) + mIsNegative(bool)
PathAsset = std::variant<Currency, MPTID> — pathfinding-only asset reference (no issuer); used inside STPathElement.
Conversion utilities in AmountConversions.h: toSTAmount, toAmount<T>, getAsset<T>. Lean→STAmount is implicit-friendly; STAmount→lean is explicit (get<> throws on type mismatch).
Units.h provides phantom-typed ValueUnit<TAG, T> (Drops, FeeLevel, FeeLevelDouble) with unit_cast<> for explicit conversion; prevents drop/fee-level mixups at compile time.
Key Invariants
- Canonical field ordering: sort by
(SerializedTypeID << 16) | fieldValue, NOT by raw Field ID bytes — wrong sort breaks signatures - Field ID encoding: 1–3 bytes; both type and field codes <16 → single byte
(type<<4)|name - Hash domain separation: every signable payload prepends a 4-byte
HashPrefix(STX\0,SMT\0,VAL\0,BCH\0,CLM\0,LWR\0,MAN,TXN, etc.) — never share hashes across domains. Helpermake_hash_prefix(a,b,c)is constexpruint32_tbuilder. - STObject access semantics:
obj[sfFoo]throwsFieldErrif absent;obj[~sfFoo]returnsstd::optional.getOrThrow<T>(name)family injson_get_or_throw.henforces presence + type for raw JSON inputs. - Amendment IDs are deterministic:
featureFoo == sha512Half(Slice("Foo"))— never change a feature name. Feature names must satisfyisFeatureName()at compile time (UpperCamelregex). Names exactly 32 bytes long are forbidden (reserved for raw hash collision prevention). numFeaturesis a ceiling, NOT an exact count. Counting includesXRPL_RETIRE_*and any inactive macros; never use it as a length.- Feature registry frozen at startup:
registerFeaturechecksnumFeaturesand aborts via static-initLogicErrorif exceeded. ThereadOnlyatomic fence flips after all file-scope variables are initialized — any query before then asserts. - Singletons everywhere:
SField,LedgerFormats,TxFormats,InnerObjectFormats,Permission,Featureregistry all use Meyer's singletons; registration completes beforemain()via static init. - Multi-sign signers MUST be sorted ascending by AccountID (no duplicates, count in [1,32], cannot include tx account). The signer's AccountID is appended to the multi-sign blob to prevent shared-RegularKey replay attacks.
vfFullyCanonicalSigalways set by signer; verifiers normalize ECDSA S to low form via libsecp256k1.- Amendment-gated arithmetic:
getSTNumberSwitchover()is aLocalValue<bool>(per-coroutine) selecting legacy vsNumber-based normalization inIOUAmount/STAmount. - TxMeta
AffectedNodesmust be sorted by index for canonical serialization (consensus-critical).addRaw()performs this sort; failure is a consensus fork risk. - STObject debug-only field-uniqueness checks (
isFieldAllowed): silent duplicate fields in production are possible bugs but no runtime check. - STLedgerEntry construction fails loudly if the type is unrecognized — no silent fallback.
- STValidation only accepts secp256k1 keys; Ed25519 keys throw at construction time.
- STNumber two-phase rounding contract:
associateAsset()must be called beforeadd(). The assertion inadd()checks idempotency — callingsetValue()afterassociateAsset()without re-associating is a programming error.
Macro-Driven Registries (X-Macros)
Single source of truth for each registry; .macro files included multiple times with redefined macros to generate enum, declarations, and definitions.
| Macro file | Used for | Add requires |
|---|---|---|
features.macro |
XRPL_FEATURE, XRPL_FIX, XRPL_RETIRE_* |
Bump numFeatures in Feature.h |
transactions.macro |
TRANSACTION(tag, value, name, delegable, amendment, privileges, fields) |
nothing — count derived |
ledger_entries.macro |
LEDGER_ENTRY(tag, value, name, rpcName, fields) + LEDGER_ENTRY_DUPLICATE for name collisions |
nothing |
sfields.macro |
TYPED_SFIELD(name, TYPE, code), UNTYPED_SFIELD |
nothing |
permissions.macro |
PERMISSION(name, txType, value) (granular permissions ≥65537) |
nothing |
Pattern uses #pragma push_macro/pop_macro to protect macro names. UNWRAP(...) strips outer parens around field-list initializers so commas don't confuse the preprocessor. LEDGER_ENTRY_DUPLICATE exists because DepositPreauth is both a transaction type and ledger entry type — JSS() can't emit the same string twice.
Feature name validation is constexpr (compile-time static_assert on the literal); typos like lower-case first letter fail to build.
The FeatureCollections internal singleton uses boost::multi_index_container with three simultaneous indexes: random-access by insertion order (byIndex for bitset mapping), hash-unique by uint256, and hash-unique by name. A simple unordered_map cannot provide the stable integer index that FeatureBitset requires.
Field Identity (SField)
- Field code =
(SerializedTypeID << 16) | fieldValue— packs type family and per-type index; canonical sort key SFieldinstances are immutable singletons created at static init viaprivate_access_tag_t(only definable insideSField.cpp)TypedField<T>adds compile-time payload type;OptionaledField<T>viaoperator~(sfField)- Metadata flags (
fieldMeta):sMD_ChangeOrig,sMD_ChangeNew,sMD_DeleteFinal,sMD_Create,sMD_Always,sMD_BaseTen(decimal display),sMD_PseudoAccount,sMD_NeedsAsset(drivesSTTakesAssetassociation),sMD_Default(field absent when zero) IsSigning::noexcludes fields from signing hash (sfTxnSignature,sfSigners,sfSignature, etc.)isBinary()⇔fieldValue<256(wire-representable);isDiscardable()⇔fieldValue>256(JSON-only, e.g.,sfHash,sfIndex)- Debug-only uniqueness check during static init; release builds will silently mis-register on collision
Wire Format Reference
| Item | Encoding |
|---|---|
| XRP STAmount | 8 bytes; bit63=0, bit62=sign(1=pos), 62-bit value |
| MPT STAmount | 8 bytes header (bit63=0, bit61=1, 56-bit value) + 192-bit MPTID |
| IOU STAmount | bit63=1, bit62=sign, 8-bit (offset+97), 54-bit mantissa, +20B currency, +20B issuer |
| AccountID | 20 bytes, VL-prefixed when standalone (STAccount mimics STBlob wire format) |
| MPTID | 192 bits = 32-bit big-endian sequence ‖ 160-bit issuer |
| STArray | elements between markers; ends with STI_ARRAY,1 (0xf1) |
| STObject | fields in canonical order; ends with STI_OBJECT,1 (0xe1) |
| VL prefix | 1 byte (0–192), 2 bytes (193–12480), 3 bytes (12481–918744); else std::overflow_error |
| STIssue | 160-bit currency; if zero → XRP; if next 160 = noAccount() → MPT (then 32-bit seq); else IOU issuer |
| STNumber | 12 bytes: int64 signed mantissa + int32 exponent (two separate statements — order must be explicit) |
| LP token currency | byte0 = 0x03; bytes 1-19 = sha512Half(min(asset1,asset2), max(asset1,asset2)) low bits |
| Order book quality | (exponent+100) << 56 | mantissa; embedded in last 8 bytes of directory key (big-endian) so SHAMap order = price order |
| NFTokenID (256-bit) | flags(2) + transferFee(2) + issuer(20) + cipheredTaxon(4) + serial(4); low 96 bits = page sort key |
| LedgerHeader | 118 bytes fixed layout (seq, drops, parentHash, txHash, accountHash, parentClose/closeTime/closeFlags, closeTimeResolution) |
| Payment channel claim | HashPrefix::paymentChannelClaim ‖ channelID(32) ‖ amount(8) |
| Batch signing payload | HashPrefix::batch ‖ outer flags(4) ‖ inner-tx count(4) ‖ inner-tx hash list |
Canonical Hashes
TXN → transactionID SND → txNode (with metadata)
MLN → leafNode MIN → innerNode
LWR → ledgerMaster STX → txSign (single-sig)
SMT → txMultiSign VAL → validation
PRP → proposal MAN → manifest
CLM → paymentChannelClaim BCH → batch
All hashes use sha512Half (first 256 bits of SHA-512). HashPrefix constants are protocol-immutable; the make_hash_prefix(a,b,c) constexpr packer in HashPrefix.h is the canonical way to declare new prefixes.
STObject and STVar
STObjectstoresstd::vector<detail::STVar>; iterators exposeSTBase const&via transform iteratorSTVaris type-erased with 72-byte inline buffer (small-object optimization);on_heap()reports whether a value spilled; larger ST types heap-allocateSTVaris movable; moving an on-heap STVar steals the pointer, while inline ones must invoke each ST type's move ctor through the v-tablecopy()/move()virtuals on every ST type delegate toSTBase::emplace()for placement-new intoSTVar's buffer- Two modes:
- Free (
mType==nullptr): linear field scan viagetFieldIndex(); accepts any field; insertion order preserved - Templated (
mTypeset): O(1) field lookup viaSOTemplate::indices_;v_laid out in template order with every slot pre-populated; unknown fields rejected
- Free (
applyTemplate()validates after deserialization;set(SOTemplate)initializes empty object with template- Deserialization depth capped at 10 to prevent stack exhaustion
operator==compares onlyisBinary()==truefields (O(n²) by design);isEquivalent()fast-paths when samemTypepointer (positional comparison)makeInnerObject()applies templates conditionally onfixInnerObjTemplate/fixInnerObjTemplate2amendments — historical ledger entries without template structure must not be rejected on replay
Proxy Access Pattern
auto amt = tx[sfAmount]; // ValueProxy: throws FieldErr if absent
auto dst = tx[~sfDestination]; // OptionalProxy: std::optional
tx[sfFlags] = 0; // proxy.assign() — soeDEFAULT zero is silently removed
tx[~sfDestTag] = std::nullopt; // remove field (only valid for soeOPTIONAL)
Proxies forbid removing soeREQUIRED or soeDEFAULT fields.
SOEStyle (Field Presence)
| Style | Meaning |
|---|---|
soeREQUIRED |
must be present |
soeOPTIONAL |
may be absent; if present, may carry default value |
soeDEFAULT |
may be absent; if present, must NOT equal default — auto-removed when assigned default |
SOETxMPTIssue flag on amount/issue fields: soeMPTSupported, soeMPTNotSupported, soeMPTNone. Omitting soeMPTSupported silently rejects MPT amounts in that field.
Common Bug Patterns
- Adding to
transactions.macrowithout adding tosfields.macro→ silent serialization failures - Forgetting to bump
numFeaturesafterXRPL_FEATURE→ static-initLogicError(registry overflow) caught at startup - Hand-built binary blobs in non-canonical field order → signature verification failures
- Omitting
soeMPTSupportedon amount field → MPT payments silently rejected - Mutating
sfTransactionTypeinsideSTTxassembler callback →LogicError(caught at startup) - Storing
STBasesubclasses directly instd::vector→ field names lost on copy-assignment slide; useSTArray/STObjectinstead - Storing
Currencyas"XRP"ISO code (badCurrency()) instead of zero → silently rejected;to_currency()legacy returnsbadCurrency()rather than failing - Forgetting to call
associateAsset(sle, asset)near end ofdoApply()for vault/loan transactors → unroundedSTNumbervalues - Returning
tec*frompreflight()→NotTECtype prevents this at compile time (would allow fee theft on unsigned tx) TxMeta::AffectedNodesleft unsorted before serialization → consensus-fork risk- Comparing
Issueinstances when one side is MPT-wrapped →Issue::operator==only compares currency+account; useAssetequality - Relying on debug-only
assertinsideSTObject::isFieldAllowedto catch duplicate fields in release - Treating
numFeaturesas a length / iteration bound (it includes retired slots) - Calling
setValue()on anSTNumberfield afterassociateAsset()without re-associating → idempotency assertion fires inadd() - Using Ed25519 key with
STValidation→ throws at construction time (only secp256k1 allowed) - Batch inner transaction
sfRawTransactionsarray exceedsmaxBatchTxCount(8) or contains nestedttBATCH→ rejected bypassesLocalChecks() getNFTokenIDFromPage()without the page-split guard: asfModifiedNodefor a third page may lacksfNFTokensinsfPreviousFields; skip silentlySTNumberJSON string parsing asserting!getCurrentTransactionRules()— string-format numbers not allowed inside active transaction processing
Key Patterns
Amendment Registration
// In features.macro:
XRPL_FEATURE(MyFeature, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (MyBugFix, Supported::yes, VoteBehavior::DefaultNo)
XRPL_RETIRE_FEATURE(OldFeature) // code removed; remains registered for ledger compat
Lifecycle: Supported::no/DefaultNo → Supported::yes/DefaultNo → (rare) DefaultYes for critical fixes. Never revert Supported::yes to no (would amendment-block existing nodes).
setCurrentTransactionRules() has a non-obvious side effect: it calls Number::setMantissaScale() to push the precision mode (small vs large) without requiring Number to query rules on every arithmetic call.
NotTEC vs TER
NotTEC preflight(...); // can only return tel/tem/tef/ter/tes (no tec)
TER doApply(...); // can return any code including tec*
TERSubset<Trait> enforces this at compile time via enable_if. TERtoInt(v) is the authorized free-function conversion (member explicit operator would be too permissive in initializer contexts).
Signing / Verifying
sign(st, HashPrefix::txSign, KeyType::secp256k1, sk); // writes sfSignature
verify(st, HashPrefix::txSign, pubKey); // returns bool
// Multi-sign optimization (shared body, per-signer suffix):
auto s = startMultiSigningData(obj);
finishMultiSigningData(signerAccountID, s); // append signer ID to shared payload
addWithoutSigningFields() excludes signature fields from the signed payload. Both sign() and verify() share the same serialization path (serialize once, check/set the field), ensuring they cannot diverge.
Batch Signing
serializeBatch() (in Batch.h, inline) produces: HashPrefix::batch ‖ outer flags(4) ‖ inner-tx count(4) ‖ inner-tx hash list. Both checkBatchSingleSign() and checkBatchMultiSign() call this once; multi-sign appends the per-signer AccountID suffix via finishMultiSigningData. passesLocalChecks() rejects nested batches.
STNumber + STTakesAsset
// Vault/Loan/LoanBroker fields use STNumber (no asset embedded).
// In doApply(), after all mutations:
associateAsset(*sle, vaultAsset); // rounds all sMD_NeedsAsset fields, removes zero-defaults
STNumber serializes a Number (signed mantissa+exponent, 12 bytes); rounding is asset-dependent and resolved by associateAsset walking fields flagged sMD_NeedsAsset. Fields with sMD_Default are removed from the SLE after rounding if the value became zero. associateAsset() is offset-based (the only path that yields mutable STBase&).
LP Token Currency Derivation
Currency lpc = ammLPTCurrency(asset1, asset2); // canonical std::minmax
// byte 0 = 0x03 (LP marker), bytes 1..19 = low 152 bits of sha512Half(min, max)
Pseudo-Account Synthesis
Pseudo-accounts (AMM, Vault, LoanBroker) carry a 256-bit synthesized ID in fields flagged sMD_PseudoAccount (sfAMMID, sfVaultID, sfLoanBrokerID). These identify a stateless account address derived from the owner ledger entry.
NFT Token ID Recovery from Metadata
getNFTokenIDFromPage() uses set-difference: collect all token IDs from sfPreviousFields and sfFinalFields across all metadata nodes; assert finalIDs.size() == prevIDs.size() + 1; use std::mismatch to find the inserted entry. Guard: when a mint causes a page split, the third page's sfModifiedNode may have sfPreviousFields without sfNFTokens — check presence before extracting.
STAmount Arithmetic Details
- IOU canonical range: mantissa ∈ [10^15, 10^16), exponent ∈ [-96, +80]; zero = (mantissa=0, exponent=-100)
- Two rounding modes:
mulRound/divRound(legacy, rounds up when fractional ≥ 0.1) vsmulRoundStrict/divRoundStrict(correct remainder tracking, propagatesNumberRoundModeGuard) - Overflow guard in multiply: if
min(a,b) > sqrt(cMaxNative), product overflows — checked before 128-bit intermediate canAdd/canSubtract: for IOU, uses round-trip relative error test with 10^-4 toleranceareComparable(): usesstd::visitoverAssetvariant; incompatible asset types throw immediately- Feature-gated:
featureSingleAssetVault/featureLendingProtocolgate thefromNumber()path inoperator=(Number const&)
QualityFunction (AMM Path Optimization)
q(out) = m * out + b where b = reciprocal-rate intercept, m = AMM price-impact slope.
AMMTag:m_ = -fee/poolIn,b_ = poolOut*fee/poolIn— derived from single-path AMM swap formulaCLOBLikeTag:m_ = 0,b_ = 1/quality.rate()— also used for multi-path AMM (fixed allocation = constant quality)combine():m_ += b_ * qf.m_; b_ *= qf.b_— linear function composition; clearsquality_cache when slope becomes nonzerooutFromAvgQ(): solvesout = (1/rate - b_) / m_; rounding modeupwardto conservatively bound output; returnsnulloptifm_==0, rate==0, orout<=0saveNumberRoundModeRAII guard scopes the upward-rounding to just this computation
AccountID Cache
Direct-mapped cache in AccountID.cpp. Indexed by hardened_hash<> (xxHash + random seed = DoS-resistant). Lock sharding: single atomic<uint64_t> locks_ encodes 64 independent spinlocks via packed_spinlock (one per index % 64). Edge case: encoding[0] != 0 guard distinguishes an uninitialized slot from a legitimate cache hit for the all-zero xrpAccount(). Cache is optional; initAccountIdCache(0) disables it entirely.
Cross-Chain Bridge Attestations
Two parallel hierarchies: Attestations::AttestationClaim / AttestationCreateAccount (full, with signature — what witnesses submit) vs XChainClaimAttestation / XChainCreateAccountAttestation (ledger-stored, signature stripped). Conversion constructors project signing→storage in one step.
AttestationMatch three-state enum: match, matchExceptDst, nonDstMismatch. XChainAddClaimAttestation requires match; XChainClaim (user-specified dst) accepts matchExceptDst. sameEvent() ignores signer identity fields; full operator== requires all fields.
Max attestations per container: 256 (far above any real witness set; guards memory allocation).
Critical Files
Foundations
include/xrpl/protocol/SField.h,src/libxrpl/protocol/SField.cpp— field registry, X-macro expansion, code packinginclude/xrpl/protocol/Feature.h,src/libxrpl/protocol/Feature.cpp—numFeatures(ceiling!),FeatureBitset,registerFeaturewith compile-time name validation;readOnlyatomic fenceinclude/xrpl/protocol/Rules.h,src/libxrpl/protocol/Rules.cpp—Rulessnapshot of enabled amendments;CurrentTransactionRulesis aLocalValue<Rules const*>(per-coroutine);isFeatureEnabled()queries thread-local;setCurrentTransactionRulespushesNumbermantissa scaleinclude/xrpl/protocol/HashPrefix.h— protocol-immutable domain separators;make_hash_prefixconstexpr packer
Macro Tables (single sources of truth)
include/xrpl/protocol/detail/features.macroinclude/xrpl/protocol/detail/transactions.macroinclude/xrpl/protocol/detail/ledger_entries.macroinclude/xrpl/protocol/detail/sfields.macroinclude/xrpl/protocol/detail/permissions.macro
Type System Roots
STBase.h/cpp— polymorphic root;emplace()SOO helper;JsonOptions;STExchangetraits glueSTObject.h/cpp— heterogeneous container, proxy system, template enforcement, debug uniqueness assertsSTVar(detail/STVar.h) — 72-byte inline variant;on_heap(); move steals pointer when heap-allocated; depth guard at 10SOTemplate.h/cpp— schema with O(1) field index; move-only; carriesSOEStyle+SOETxMPTIssue
Format Registries
TxFormats.h/cpp,LedgerFormats.h/cpp,InnerObjectFormats.h/cpp— all inheritKnownFormats<Key, Derived>withforward_list<Item>(pointer-stable) + dual flat_mapsLedgerEntryrpcName vs name distinction enablesDepositPreauthcollision handling
Amount / Asset Stack
Asset.h/cpp— variant of Issue/MPTIssue;visit(),equalTokens(),BadAssetsentinelIssue.h/cpp,MPTIssue.h/cpp— XRP/IOU and MPT identity; noteIssue::operator==ignores MPT-ness — always go throughAssetSTAmount.h/cpp— unified serialized amount;canMul/canAdd/canSubtractsafety checks;mulRound/mulRoundStrict(legacy vs precise rounding);roundToScaleSTNumber.h/cpp—Number-typed field; pairs withSTTakesAssetinfrastructure; 12-byte wire: int64 mantissa + int32 exponentSTIssue.h/cpp,STCurrency.h/cpp— asset-only fieldsSTTakesAsset.h/cpp—associateAssetwalkssMD_NeedsAssetfields, rounds + strips zero-defaults; include order:STTakesAsset.hbeforeSTLedgerEntry.hIOUAmount.h/cpp,XRPAmount.h,MPTAmount.h/cpp— lean representationsRate2.h—Ratenewtype withparityRate = 1_000_000_000; transfer-rate mathUnits.h— phantom-typedDrops/FeeLevelNumber(inxrpl/basics/) — high-precision arithmetic;MantissaRange::largeenabled by SingleAssetVault/LendingProtocol amendmentsAmountConversions.h— typed coercions
Cryptography
PublicKey.h/cpp— 33-byte unified format (0xED prefix for Ed25519);ECDSACanonicalityenum (canonical vs fullyCanonical); libsecp256k1 normalizationSecretKey.h/cpp—secure_erasein dtor; deleted==/<<; XRPL-specific secp256k1 derivation viaGeneratorSeed.h/cpp— 128-bit;parseGenericSeed()cascades hex→base58→RFC1751→passphrase, rejecting other key types firstdetail/secp256k1.h— libsecp256k1 context singleton via template-with-default-param trick (ODR-safe header-only); created withSIGN|VERIFYflags combineddigest.h—sha512Half,sha512_half_hasher_s(secure erase variant)tokens.h/cpp(+b58_utils.h,token_errors.h) — Base58Check; fast path uses base 58^10 intermediate (10–15× speedup viaunsigned __int128, gated on non-MSVC);TokenTypeenum is protocol-stable;alphabetReverseisconstexpr256-element array; leading-zero bytes each map to'r'
Wire I/O
Serializer.h/cpp— accumulator;addVL,addFieldID, big-endian integers,getSHA512Half()SerialIter— non-owning forward cursor over a byte buffer; throws on underrunSign.h/cpp—sign/verifywith HashPrefix prepended toaddWithoutSigningFields()output;startMultiSigningData/finishMultiSigningDatasplit for batch-signer optimization;signingForIDhelper for arbitrary payload bytesBatch.h— inlineserializeBatch():HashPrefix::batch ‖ flags(4) ‖ count(4) ‖ txidsserialize.h— top-level convenience helpersmessages.h— protobuf message tag constants (TYPE_BOOLundef guard documented)
Higher-Level Objects
STTx.h/cpp— cachestid_andtx_type_;passesLocalChecks(memos, pseudo-tx, MPT support, batch nesting, max 8 inner txs);sterilize()round-trip;getBatchTransactionIDs()lazy + immutable after first call;getFeePayer()returnssfDelegateorsfAccount;checkSign()dispatches single/multi/batch/counterparty; SQL helpers (getMetaSQL)STLedgerEntry.h/cpp(aliasSLE) — typed ledger object;thread()updatessfPreviousTxnID;isThreadedType()gated byfixPreviousTxnID;getJson()injectsjss::indexand syntheticmpt_issuance_idfor MPT issuancesSTValidation.h/cpp— lazyvalid_cache;mTrustedseparate from validity;lookupNodeIDcallback decouples manifest system;validationFormat()is function-local static (SField init order safety);sfCookieissoeDEFAULTto prevent fingerprintingSTArray.h/cpp,STVector256.h/cpp,STBitString.h/cpp,STInteger.h/cpp,STBlob.h/cpp,STAccount.h/cpp,STPathSet.h/cpp,STXChainBridge.h/cpp
Transaction Meta
TxMeta.h/cpp—AffectedNodes(sorted by index inaddRaw()!),DeliveredAmount,sfParentBatchID; linear scan for node lookup (bounded by 32-slot reservation);getAffectedAccounts()must match JSMeta#getAffectedAccountsLedgerHeader.h/cpp— 118-byte fixed serialization; close-time-resolution fudging
Indexes and Keys
Indexes.h/cpp—keylet::*factories withLedgerNameSpacetagged hashing;keylet::quality()embeds 64-bit quality in last 8 bytes (big-endian);keylet::amm()usesstd::minmax+if constexprfor heterogeneous token types;nftpage= owner(160 bits) ‖ masked token(96 bits) — range scan, no hashKeylet.h/cpp— type-tagged(uint256, LedgerEntryType);ltANYwildcard,ltCHILDrejects directoriesProtocol.h— protocol-wide constants (FLAG_LEDGER_INTERVAL, etc.)nftPageMask.h,nft.h— NFT page boundary (low 96 bits); composite keys (high 160 = owner AccountID)NFTokenID.h/cpp— flags(2)+fee(2)+issuer(20)+cipheredTaxon(4)+serial(4); LCG384160001 * seq + 2459ciphers taxon;getNFTokenIDFromPage()andgetNFTokenIDFromDeletedOffer()for metadata enrichmentNFTokenOfferID.h/cpp,NFTSyntheticSerializer.h/cpp— derived/synthetic NFT entries; consumed by Clio as public APIBook.h—(in_asset, out_asset)order-book identitySeqProxy.h/cpp— sequence vs ticket abstraction; sequence-type values sort before ticket-type values
Validation Helpers (return NotTEC, preflight-time)
AMMCore.h/cpp—invalidAMMAsset,invalidAMMAssetPair,invalidAMMAmount;ammLPTCurrency()uses canonicalstd::minmaxPermissions.h/cpp— singleton;isDelegable()checks granular vs transaction-level (<UINT16_MAXboundary), amendment, delegable flag
RPC / JSON Boundary
STParsedJSON.h/cpp— depth cap 64; field-path-qualified errors viamake_name; recognizes"Payment","tesSUCCESS", etc.ErrorCodes.h/cpp— append-only enum;sortedErrorInfosvalidated at compile time;warning_code_idistinct fromerror_code_iRPCErr.h/cpp—RPC::Status/make_errorhelpersApiVersion.h—apiMinimumSupportedVersion(1),apiMaximumSupportedVersion(2),apiBetaVersion(3);apiVersionIfUnspecified(1);forAllApiVersions/forApiVersionstemplates pass version asintegral_constantfor compile-time branching;getAPIVersionNumber()returnsapiInvalidVersion(0) on parse failure;setVersion()has v1 legacy semver-string shimMultiApiJson.h— per-API-versionJson::Valuearray indexed[version - RPC::apiMinimumVersion]; composes withforAllApiVersionsfromApiVersion.h; preserves wire compatibility across versionsjss.h— every JSON key asJson::StaticStringviaJSS(name)macro; PascalCase = protocol fields, snake_case = RPCjson_get_or_throw.h—getOrThrow<T>(jv, name)specializations enforce presence + type; standard idiom for parsing untrusted JSONst.h— convenience aggregate header for all ST types
Specialized Types
STIssue,STAccount(160-bit, VL-encoded),STBitString<Bits>,STInteger<T>,STBlob,STArray,STVector256,STCurrency,STPathSet,STXChainBridge,STNumber(asset-contextual)Quality.h/cpp— inverted encoding (lower uint64 = higher quality);ceil_in/ceil_outproportional scaling;_strictvariants honor Number rounding modeQualityFunction.h/cpp— linearq(out)=m*out+b; AMMTag (slope from pool) vs CLOBLikeTag (m=0);combine()for multi-step strands;outFromAvgQ()solves for capped outputXChainAttestations.h/cpp—Attestations::namespace (full, with signature) vsxrpl::(stored);match()returns three-stateAttestationMatch
Pseudo-Account Fields (sMD_PseudoAccount)
sfAMMID,sfVaultID,sfLoanBrokerID— 256-bit hash representing a synthesized account address
Misc / System
BuildInfo.h/cpp— version string,getVersionString()consumed by manifest/handshakeSystemParameters.h— drops-per-XRP,INITIAL_XRP, ledger-related constants; validated bystatic_assertUintTypes.h—uint256/uint160/uint128aliases and tagged variants (Currency,NodeID, etc.)TER.h/cpp— error code enum families +TERSubsetTxFlags.h— X-macro driven flag tables (tf*); see TxFlags Architecture belowTxFormats.h/cpp— transaction-type → field schemaAccountID.h/cpp—calcAccountID()= SHA-256 then RIPEMD-160 (matches Bitcoin for security argument);AccountIdCachedirect-mapped with spinlock sharding
Numeric Encoding Reference
IOU canonical: mantissa ∈ [10^15, 10^16), exponent ∈ [-96, +80]
zero = (mantissa=0, exponent=-100) — sorts below smallest positive
XRP max: cMaxNativeN = 10^17 drops (100 billion XRP)
MPT max: maxMPTokenAmount = INT64_MAX = 0x7FFFFFFFFFFFFFFF
Transfer rate: Rate{value} where value/1_000_000_000 = 1.0 (parityRate = 1:1)
NFT transfer fee: uint16 basis points (0–50000), convert via nft::transferFeeAsRate (×10000)
AMM auction fee: basis points; trading fee in tenths of basis points (10000 = 1%)
STNumber mantissa: int64 signed; when MantissaRange::large active, accessor divides by 10 to fit wire format
Protocol-Stable Constants (NEVER CHANGE)
LedgerEntryTypenumeric values (in ledger objects)TxTypenumeric values (in signed transactions)SerializedTypeIDandSFieldcodes (in serialized fields)LedgerNameSpacediscriminator characters (in keylet derivation) — legacyCONTRACT,GENERATOR,NICKNAMEreserved even though deprecatedHashPrefixenum values (in signature/hash domain separation)error_code_iandwarning_code_inumeric values (clients depend on them; append-only)TECcodes(and otherTERfamily numeric values) — recorded in transaction metadataTokenType(Base58Check prefix bytes for accounts/seeds/nodes)- LP token currency prefix byte (
0x03) - Universal transaction flags (
tfFullyCanonicalSig,tfInnerBatchTxn) FLAG_LEDGER_INTERVAL = 256(drives consensus timing)INITIAL_XRP = 100B × 10^6 drops(validated bystatic_assertagainstNumber::maxRep)- NFT taxon LCG constants (
384160001 * seq + 2459) - All flag bit values (
tf*,lsf*,asf*) - XRPL Base58 alphabet (first char
'r'forAccountID=0is cosmetically significant) maxBatchTxCount = 8(inner transactions per batch)
Changing any requires an amendment with explicit detection logic for old/new behavior.
TxFlags Architecture
TxFlags.h is itself X-macro driven. Per-transaction flag groups are declared so that:
- Each group has
tf*named bit constants tfUniversalMaskis the union of universal flags (tfFullyCanonicalSig,tfInnerBatchTxn)- Per-transaction
tf*Maskconstants are auto-computed viaMASK_ADJso that mask matches the declared flags exactly — adding a flag automatically updates the mask TF_FLAG2marks flags whose meaning was changed by an amendment; old/new bits coexist with disjoint enable conditions- Inner-batch flag
tfInnerBatchTxnis special: marks a tx as a member of a batch (skipped by ordinary preflight signature checks)
Pattern: when adding a new flag, define it in TxFlags.h in the appropriate group; do NOT manually adjust the mask — the macro derives it.
STTx Construction Paths
Three constructors, all terminate with tid_ = getHash(HashPrefix::transactionID):
- Wire (
SerialIter&) — hottest path; enforcestxMinSizeBytes(32) andtxMaxSizeBytes(1 MB) before field parsing;set(sit)returningtrue(inner object terminator found at top level) → throws - Object promotion (
STObject&&) — no size check;applyTemplateenforces conformance - Programmatic (
TxType, assembler) — installs template first; assertssfTransactionTypeunchanged after assembler runs;LogicError(notstd::runtime_error) on mutation
getSeqProxy() unifies sfSequence (classic) and sfTicketSequence (ticket); when sfSequence==0 and sfTicketSequence present → ticket mode. Sequence-type always sorts before ticket-type.
getFeePayer() returns sfDelegate if present, else sfAccount. Authorization is enforced in Transactor::checkPermission, not here.
Counterparty Signing (sfCounterpartySignature)
Used by LoanSet — allows a second party to sign the same transaction. checkSign(Rules const&) checks primary then counterparty (if field present). Errors from counterparty check are prefixed "Counterparty: ". sign() accepts optional signatureTarget reference to write into a sub-object.