regen skills

This commit is contained in:
Denis Angell
2026-05-13 19:35:26 +02:00
parent 6f45f8036f
commit 196707b242
2 changed files with 138 additions and 42 deletions

View File

@@ -11,7 +11,9 @@ XRPL supports secp256k1 (ECDSA) and ed25519 key types. All crypto uses OpenSSL +
- Base58 encoding includes a type byte prefix and 4-byte checksum (double SHA-256)
- All randomness for cryptographic material flows through `crypto_prng()`; never call OpenSSL's `RAND_bytes` directly and never use `std::rand`/`rand()`
- `csprng_engine` is non-copyable and non-movable by deleted ops; the singleton must be accessed by reference via `crypto_prng()`
- `csprng_engine` satisfies the C++ *UniformRandomNumberEngine* named requirement (`result_type` = `std::uint64_t`, `operator()()`, `constexpr min()`/`max()`) — plugs into `std::uniform_int_distribution`, `beast::rngfill`, etc.
- RFC 1751 dictionary has exactly 2^11 = 2048 entries; entries 0570 are 13 char words, 5712047 are exactly 4 chars (used to split binary search range in `wsrch`)
- Each RFC 1751 word encodes exactly 11 bits; a 64-bit block uses 6 words (66 bits = 64 data + 2 parity); a 128-bit key uses two such blocks → 12 words
## Common Bug Patterns
@@ -24,6 +26,9 @@ XRPL supports secp256k1 (ECDSA) and ed25519 key types. All crypto uses OpenSSL +
- Constructing a second `csprng_engine` instance: forbidden by deleted ctors; sharing one OpenSSL pool through the singleton is required
- Passing `mix_entropy` a buffer and assuming OpenSSL credits it as entropy — the entropy estimate is always 0 (deliberately conservative)
- RFC 1751 decode: distinguish `0` (unknown word), `-1` (malformed input), `-2` (parity failure) — don't collapse all failures into a single error
- Forgetting that `insert()` in RFC1751 uses bitwise OR, not assignment — output buffer must start zero-initialized
- Treating RFC 1751 parity as cryptographic integrity — it's a 2-bit transcription check, not a MAC
- Using `getWordFromBlob` for anything cryptographic — it's a Jenkins hash and explicitly insecure
## Review Checklist
@@ -32,6 +37,7 @@ XRPL supports secp256k1 (ECDSA) and ed25519 key types. All crypto uses OpenSSL +
- Verify that key type dispatch handles both secp256k1 and ed25519 (or explicitly rejects one with a clear error)
- Any new sensitive type should follow the `SecretKey`/`Seed` pattern: destructor calls `secure_erase` as its first/only action
- New OpenSSL touchpoints should respect the `OPENSSL_VERSION_NUMBER < 0x10100000L` thread-safety guard pattern used in `csprng.cpp`
- CSPRNG failures (`RAND_bytes`/`RAND_poll` ≠ 1) must propagate via `Throw<>` (logs stack trace) — never silently fall back
## Key Patterns
@@ -49,7 +55,7 @@ SecretKey sk(Slice{buf, sizeof(buf)});
secure_erase(buf, sizeof(buf)); // MUST erase raw buffer
```
`secure_erase` delegates to `OPENSSL_cleanse`, which uses volatile writes / opaque function-pointer calls to defeat dead-store elimination. Lives in a separate TU (`secure_erase.cpp`) so the call site cannot inline it away. It does **not** clear CPU registers or caches — it is best-effort for heap/stack only (see Percival 2014).
`secure_erase` delegates to `OPENSSL_cleanse`, which uses volatile writes / opaque function-pointer calls to defeat dead-store elimination. Lives in a separate TU (`secure_erase.cpp`) so the call site cannot inline it away — the out-of-line call alone forces the compiler to treat it as an opaque side effect. It does **not** clear CPU registers or caches — it is best-effort for heap/stack only (see Percival 2014). Takes raw `void*` + byte count with no null/zero guards; callers must supply valid arguments.
### CSPRNG Usage
```cpp
@@ -64,7 +70,7 @@ rng(buf, sizeof(buf)); // operator()(void*, size_t)
beast::rngfill(buf, sizeof(buf), crypto_prng());
```
`csprng_engine` satisfies the C++ *UniformRandomNumberEngine* named requirement, so it plugs directly into `std::uniform_int_distribution` and similar. Failure (insufficient entropy) throws `std::runtime_error` via `Throw<>`; callers generally do not catch — propagation halts the operation, which is correct.
Failure (insufficient entropy) throws `std::runtime_error("CSPRNG: Insufficient entropy")` via `Throw<>`; callers generally do not catch — propagation halts the operation, which is correct. Generating cryptographic material from an entropy-exhausted pool would be worse than crashing.
### Key Type Dispatch
```cpp
@@ -90,6 +96,8 @@ int rc = RFC1751::getKeyFromEnglish(roundTrip, words);
`Seed.cpp` reverses the 16 bytes before/after RFC 1751 encoding to match the RFC's big-endian convention. `standard()` normalizes input by uppercasing and applying visual substitutions `1→L`, `0→O`, `5→S` for handwritten/OCR tolerance. The 2-bit parity per 8-byte half is a transcription check, **not** a cryptographic integrity check.
`getKeyFromEnglish` uses `boost::algorithm::split` with `token_compress_on` for whitespace tolerance. The asymmetry between encoder (no return code — cannot fail on valid input) and decoder (4-valued return code) is intentional: encoding is lossless, decoding must validate user input.
`getWordFromBlob` is a separate utility: Jenkins one-at-a-time hash → `% 2048` → one dictionary word. Explicitly **not** cryptographically secure; used in `NetworkOPs.cpp` for `shroudedHostId` (privacy-preserving node label in logs/RPC).
## Module Layout
@@ -108,26 +116,29 @@ These three are used together by the protocol-level key/seed types (`SecretKey`,
- Constructor calls `RAND_poll()` eagerly to surface OS entropy failures at startup, not at first key gen
- Destructor calls `RAND_cleanup()` only for OpenSSL `< 1.1.0` (modern versions clean up via `atexit`)
- Thread-safety mutex is compile-time gated: `#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || !defined(OPENSSL_THREADS)` — modern builds elide the lock on the hot path because `RAND_bytes` is internally thread-safe
- `mix_entropy` always holds the mutex around `RAND_add`; reads from `std::random_device` happen before locking (independently thread-safe)
- `mix_entropy` passes entropy estimate `0` to `RAND_add` — never claim entropy for `std::random_device` or caller-supplied buffers (they may be weak on some platforms)
- Thread-safety mutex is compile-time gated: `#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || !defined(OPENSSL_THREADS)` — modern builds elide the lock on the hot path because `RAND_bytes` is internally thread-safe. The mutex is *always* held around `RAND_add` in `mix_entropy` regardless of version.
- `mix_entropy` reads 128 values from `std::random_device` *before* locking (independently thread-safe), then locks for `RAND_add`
- `mix_entropy` passes entropy estimate `0` to `RAND_add` — never claim entropy for `std::random_device` or caller-supplied buffers (they may be weak on some platforms; conservative accounting prevents prematurely satisfying OpenSSL's seeding threshold)
- Called on a timer from `Application.cpp` to stir fresh OS entropy during the node's lifetime
- Singleton is a function-local `static` (Meyers singleton); C++11 guarantees thread-safe one-time init
- Scalar `operator()()` delegates to buffer-fill overload with `sizeof(result_type)` (8 bytes) — both paths share validation/error handling
- Wrapping behind a single `xrpl::secure_erase` function lets the strategy change (e.g., to `explicit_bzero`) at one auditable choke point without touching call sites
### RFC 1751 Internals Worth Knowing
- `extract(s, start, length)` / `insert(s, x, start, length)`: read/write `length ≤ 11` bits at arbitrary offset across a 9-byte buffer; guarded by `XRPL_ASSERT` (stripped in release)
- `extract(s, start, length)` / `insert(s, x, start, length)`: read/write `length ≤ 11` bits at arbitrary offset across a 9-byte buffer; guarded by `XRPL_ASSERT` (stripped in release). Both work across byte boundaries by assembling 23 adjacent bytes.
- `insert` uses bitwise OR (not assignment), so the output buffer must start zero-initialized; partial writes accumulate safely
- `btoe` adds a 9th byte for the 2-bit parity computed by summing all 32 pairs of bits across the 64-bit payload; parity occupies bit positions 6465
- `etob` validates: exactly 6 words, each 14 chars, all in dictionary, parity matches — distinct error codes per failure mode
- `getKeyFromEnglish` uses `boost::algorithm::split` with `token_compress_on` for whitespace tolerance
- `etob` validates: exactly 6 words, each 14 chars, all in dictionary, parity matches — distinct error codes per failure mode (`0` unknown, `-1` malformed, `-2` parity)
- `wsrch` halves the binary search range based on input word length: `[0, 571)` for 13 char words, `[571, 2048)` for 4-char words
- No exceptions used anywhere in RFC1751 — all errors are integer return codes
## Key Files
- `include/xrpl/protocol/SecretKey.h` / `PublicKey.h` — key types
- `src/libxrpl/protocol/SecretKey.cpp` — signing, key generation; canonical example of CSPRNG + `secure_erase` discipline
- `src/libxrpl/protocol/PublicKey.cpp` — verification
- `src/libxrpl/protocol/Seed.cpp` — 128-bit seed; uses RFC 1751 for mnemonic encoding
- `src/libxrpl/protocol/Seed.cpp` — 128-bit seed; uses RFC 1751 for mnemonic encoding (reverses bytes for big-endian convention)
- `include/xrpl/protocol/digest.h` — hash functions (`sha512Half`, `ripesha_hasher`, etc.)
- `include/xrpl/crypto/csprng.h` + `src/libxrpl/crypto/csprng.cpp` — CSPRNG engine and singleton
- `include/xrpl/crypto/secure_erase.h` + `src/libxrpl/crypto/secure_erase.cpp` — memory wipe primitive

View File

@@ -1,6 +1,6 @@
# 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, and the typed object model (`STBase` hierarchy) that every transaction and ledger object inhabits.
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
@@ -19,19 +19,27 @@ STAmount = unified wire/serialized form holding Asset + value
- canonicalize() normalizes (mantissa, exponent) per asset rules
```
`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:** 13 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`, `STX\0`, `BCH\0`, `CLM\0`, `LWR\0`, etc.) never share hashes across domains
- **STObject access semantics:** `obj[sfFoo]` throws `FieldErr` if absent; `obj[~sfFoo]` returns `std::optional`
- **Amendment IDs are deterministic:** `featureFoo == sha512Half(Slice("Foo"))` never change a feature name
- **Singletons everywhere:** `SField`, `LedgerFormats`, `TxFormats`, `InnerObjectFormats`, `Permission`, `Feature` registry all use Meyer's singletons; registration completes before `main()` via static init
- **Multi-sign signers MUST be sorted ascending by AccountID** (no duplicates, count in [1,32], cannot include tx account)
- **`vfFullyCanonicalSig` always set** by signer; verifiers normalize ECDSA S to low form
- **Amendment-gated arithmetic:** `getSTNumberSwitchover()` is a `LocalValue<bool>` (per-coroutine) selecting legacy vs `Number`-based normalization in `IOUAmount`/`STAmount`
- **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. Helper `make_hash_prefix(a,b,c)` is constexpr `uint32_t` builder.
- **STObject access semantics:** `obj[sfFoo]` throws `FieldErr` if absent; `obj[~sfFoo]` returns `std::optional`. `getOrThrow<T>(name)` family in `json_get_or_throw.h` enforces presence + type for raw JSON inputs.
- **Amendment IDs are deterministic:** `featureFoo == sha512Half(Slice("Foo"))` never change a feature name. Feature names must satisfy `isFeatureName()` at compile time (`UpperCamel` regex).
- **`numFeatures` is a ceiling, NOT an exact count.** Counting includes `XRPL_RETIRE_*` and any inactive macros; never use it as a length.
- **Feature registry frozen at startup:** `registerFeature` checks `numFeatures` and aborts via static-init `LogicError` if exceeded.
- **Singletons everywhere:** `SField`, `LedgerFormats`, `TxFormats`, `InnerObjectFormats`, `Permission`, `Feature` registry all use Meyer's singletons; registration completes before `main()` via static init.
- **Multi-sign signers MUST be sorted ascending by AccountID** (no duplicates, count in [1,32], cannot include tx account).
- **`vfFullyCanonicalSig` always set** by signer; verifiers normalize ECDSA S to low form via libsecp256k1.
- **Amendment-gated arithmetic:** `getSTNumberSwitchover()` is a `LocalValue<bool>` (per-coroutine) selecting legacy vs `Number`-based normalization in `IOUAmount`/`STAmount`.
- **TxMeta `AffectedNodes` must be sorted by index** for canonical serialization (consensus-critical).
- **STObject debug-only field-uniqueness checks** (`isFieldAllowed`): silent duplicate fields in production are possible bugs but no runtime check.
## Macro-Driven Registries (X-Macros)
@@ -47,6 +55,8 @@ Single source of truth for each registry; `.macro` files included multiple times
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.
## Field Identity (`SField`)
- **Field code** = `(SerializedTypeID << 16) | fieldValue` packs type family and per-type index; canonical sort key
@@ -55,6 +65,7 @@ Pattern uses `#pragma push_macro/pop_macro` to protect macro names. `UNWRAP(...)
- Metadata flags (`fieldMeta`): `sMD_ChangeOrig`, `sMD_ChangeNew`, `sMD_DeleteFinal`, `sMD_Create`, `sMD_Always`, `sMD_BaseTen` (decimal display), `sMD_PseudoAccount`, `sMD_NeedsAsset` (drives `STTakesAsset` association)
- `IsSigning::no` excludes 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
@@ -64,6 +75,7 @@ Pattern uses `#pragma push_macro/pop_macro` to protect macro names. `UNWRAP(...)
| MPT STAmount | 8 bytes header (bit63=0, bit61=1) + 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 (0192), 2 bytes (19312480), 3 bytes (12481918744); else `std::overflow_error` |
@@ -71,6 +83,9 @@ Pattern uses `#pragma push_macro/pop_macro` to protect macro names. `UNWRAP(...)
| 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` inner-tx hash list |
## Canonical Hashes
@@ -83,12 +98,13 @@ PRP → proposal MAN → manifest
CLM → paymentChannelClaim BCH → batch
```
All hashes use `sha512Half` (first 256 bits of SHA-512). `HashPrefix` constants are protocol-immutable.
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
- `STObject` stores `std::vector<detail::STVar>`; iterators expose `STBase const&` via transform iterator
- `STVar` is type-erased with 72-byte inline buffer (small-object optimization); larger types heap-allocate
- `STVar` is type-erased with 72-byte inline buffer (small-object optimization); `on_heap()` reports whether a value spilled; larger ST types heap-allocate
- `STVar` is movable; moving an on-heap STVar steals the pointer, while inline ones must invoke each ST type's move ctor through the v-table
- `copy()`/`move()` virtuals on every ST type delegate to `STBase::emplace()` for placement-new into `STVar`'s buffer
- **Two modes:**
- **Free** (`mType==nullptr`): linear field scan, accepts any field
@@ -120,7 +136,7 @@ Proxies forbid removing `soeREQUIRED` or `soeDEFAULT` fields.
## Common Bug Patterns
- Adding to `transactions.macro` without adding to `sfields.macro` silent serialization failures
- Forgetting to bump `numFeatures` after `XRPL_FEATURE` out-of-bounds access in `FeatureBitset`
- Forgetting to bump `numFeatures` after `XRPL_FEATURE` static-init `LogicError` (registry overflow) caught at startup
- Hand-built binary blobs in non-canonical field order signature verification failures
- Omitting `soeMPTSupported` on amount field MPT payments silently rejected
- Mutating `sfTransactionType` inside `STTx` assembler callback `LogicError` (caught at startup)
@@ -128,6 +144,10 @@ Proxies forbid removing `soeREQUIRED` or `soeDEFAULT` fields.
- Storing `Currency` as `"XRP"` ISO code (`badCurrency()`) instead of zero silently rejected; `to_currency()` legacy returns `badCurrency()` rather than failing
- Forgetting to call `associateAsset(sle, asset)` near end of `doApply()` for vault/loan transactors unrounded `STNumber` values
- Returning `tec*` from `preflight()` `NotTEC` type prevents this at compile time (would allow fee theft on unsigned tx)
- `TxMeta::AffectedNodes` left unsorted before serialization consensus-fork risk
- Comparing `Issue` instances when one side is MPT-wrapped `Issue::operator==` only compares currency+account; use `Asset` equality
- Relying on debug-only `assert` inside `STObject::isFieldAllowed` to catch duplicate fields in release
- Treating `numFeatures` as a length / iteration bound (it includes retired slots)
## Key Patterns
@@ -151,7 +171,7 @@ 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
### Signing / Verifying
```cpp
sign(st, HashPrefix::txSign, KeyType::secp256k1, sk); // writes sfSignature
@@ -164,6 +184,10 @@ finishMultiSigningData(signerAccountID, s); // append signer ID to shared paylo
`addWithoutSigningFields()` excludes signature fields from the signed payload this is what breaks the circularity.
### Batch Signing
`HashPrefix::batch` + inner-tx hash list is signed by all inner accounts; outer tx carries the assembled `sfRawTransactions` array. `passesLocalChecks()` rejects nested batches.
### STNumber + STTakesAsset
```cpp
@@ -172,13 +196,26 @@ finishMultiSigningData(signerAccountID, s); // append signer ID to shared paylo
associateAsset(*sle, vaultAsset); // rounds all sMD_NeedsAsset fields, removes zero-defaults
```
`STNumber` serializes a `Number` (signed mantissa+exponent) directly; rounding is asset-dependent and resolved by `associateAsset` walking fields flagged `sMD_NeedsAsset`.
### LP Token Currency Derivation
```cpp
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.
## Critical Files
### Foundations
- `include/xrpl/protocol/SField.h`, `src/libxrpl/protocol/SField.cpp` field registry, X-macro expansion
- `include/xrpl/protocol/Feature.h`, `src/libxrpl/protocol/Feature.cpp` `numFeatures`, `FeatureBitset`, registration
- `include/xrpl/protocol/Rules.h` per-coroutine active amendment set; `isFeatureEnabled()` queries thread-local
- `include/xrpl/protocol/HashPrefix.h` protocol-immutable domain separators
- `include/xrpl/protocol/SField.h`, `src/libxrpl/protocol/SField.cpp` field registry, X-macro expansion, code packing
- `include/xrpl/protocol/Feature.h`, `src/libxrpl/protocol/Feature.cpp` `numFeatures` (ceiling!), `FeatureBitset`, `registerFeature` with compile-time name validation
- `include/xrpl/protocol/Rules.h`, `src/libxrpl/protocol/Rules.cpp` `Rules` snapshot of enabled amendments; `CurrentTransactionRules` is a `LocalValue<Rules const*>` (per-coroutine); `isFeatureEnabled()` queries thread-local
- `include/xrpl/protocol/HashPrefix.h` protocol-immutable domain separators; `make_hash_prefix` constexpr packer
### Macro Tables (single sources of truth)
- `include/xrpl/protocol/detail/features.macro`
@@ -188,62 +225,93 @@ associateAsset(*sle, vaultAsset); // rounds all sMD_NeedsAsset fields, removes
- `include/xrpl/protocol/detail/permissions.macro`
### Type System Roots
- `STBase.h/cpp` polymorphic root, `emplace()` SOO helper, `JsonOptions`
- `STObject.h/cpp` heterogeneous container, proxy system, template enforcement
- `STVar` (`detail/STVar.h`) 72-byte inline variant, depth guard at 10
- `SOTemplate.h/cpp` schema with O(1) field index; move-only
- `STBase.h/cpp` polymorphic root; `emplace()` SOO helper; `JsonOptions`; `STExchange` traits glue
- `STObject.h/cpp` heterogeneous container, proxy system, template enforcement, debug uniqueness asserts
- `STVar` (`detail/STVar.h`) 72-byte inline variant; `on_heap()`; move steals pointer when heap-allocated; depth guard at 10
- `SOTemplate.h/cpp` schema with O(1) field index; move-only; carries `SOEStyle` + `SOETxMPTIssue`
### Format Registries
- `TxFormats.h/cpp`, `LedgerFormats.h/cpp`, `InnerObjectFormats.h/cpp` all inherit `KnownFormats<Key, Derived>` with `forward_list<Item>` (pointer-stable) + dual flat_maps
- `LedgerEntry` rpcName vs name distinction enables `DepositPreauth` collision handling
### Amount/Asset Stack
### Amount / Asset Stack
- `Asset.h/cpp` variant of Issue/MPTIssue; `visit()`, `equalTokens()`, `BadAsset` sentinel
- `Issue.h/cpp`, `MPTIssue.h/cpp` XRP/IOU and MPT identity
- `STAmount.h/cpp` unified serialized amount; `canMul`/`canAdd`/`canSubtract` safety checks; `mulRound`/`mulRoundStrict` (legacy vs precise rounding)
- `Issue.h/cpp`, `MPTIssue.h/cpp` XRP/IOU and MPT identity; **note** `Issue::operator==` ignores MPT-ness always go through `Asset`
- `STAmount.h/cpp` unified serialized amount; `canMul`/`canAdd`/`canSubtract` safety checks; `mulRound`/`mulRoundStrict` (legacy vs precise rounding); `roundToScale`
- `STNumber.h/cpp` `Number`-typed field; pairs with `STTakesAsset` infrastructure
- `STIssue.h/cpp`, `STCurrency.h/cpp` asset-only fields
- `STTakesAsset.h/cpp` `associateAsset` walks `sMD_NeedsAsset` fields, rounds + strips zero-defaults
- `IOUAmount.h/cpp`, `XRPAmount.h`, `MPTAmount.h/cpp` lean representations
- `Rate2.h` `Rate` newtype with `parityRate = 1_000_000_000`; transfer-rate math
- `Units.h` phantom-typed `Drops`/`FeeLevel`
- `Number` (in `xrpl/basics/`) high-precision arithmetic; `MantissaRange::large` enabled by SingleAssetVault/LendingProtocol amendments
- `AmountConversions.h` typed coercions
### Cryptography
- `PublicKey.h/cpp` 33-byte unified format (0xED prefix for Ed25519); `ECDSACanonicality` enum (canonical vs fullyCanonical), libsecp256k1 normalization
- `PublicKey.h/cpp` 33-byte unified format (0xED prefix for Ed25519); `ECDSACanonicality` enum (canonical vs fullyCanonical); libsecp256k1 normalization
- `SecretKey.h/cpp` `secure_erase` in dtor; deleted `==`/`<<`; XRPL-specific secp256k1 derivation via `Generator`
- `Seed.h/cpp` 128-bit; `parseGenericSeed()` cascades hexbase58RFC1751passphrase, rejecting other key types first
- `secp256k1.h` libsecp256k1 context singleton
- `digest.h` `sha512Half`, `sha512_half_hasher_s` (secure erase variant)
- `tokens.h/cpp` Base58Check; fast path uses base 58^10 intermediate (1015× speedup, gated on non-MSVC for `__int128`)
- `tokens.h/cpp` (+ `b58_utils.h`, `token_errors.h`) Base58Check; fast path uses base 58^10 intermediate (1015× speedup, gated on non-MSVC for `__int128`); `TokenType` enum is protocol-stable
### Wire I/O
- `Serializer.h/cpp` accumulator; `addVL`, `addFieldID`, big-endian integers, `getSHA512Half()`
- `SerialIter` non-owning forward cursor over a byte buffer; throws on underrun
- `Sign.h/cpp` `sign`/`verify` with HashPrefix prepended to `addWithoutSigningFields()` output
- `Sign.h/cpp` `sign`/`verify` with HashPrefix prepended to `addWithoutSigningFields()` output; `signingForID` helper for arbitrary payload bytes
- `serialize.h` top-level convenience helpers
- `messages.h` protobuf message tag constants
### Higher-Level Objects
- `STTx.h/cpp` caches `tid_` and `tx_type_`; `passesLocalChecks` (memos, pseudo-tx, MPT support, batch nesting); `sterilize()` round-trip
- `STLedgerEntry.h/cpp` (alias `SLE`) typed ledger object; `thread()` updates `sfPreviousTxnID`; `isThreadedType()` gated by `fixPreviousTxnID`
- `STValidation.h/cpp` lazy `valid_` cache; `mTrusted` separate from validity; `lookupNodeID` callback decouples manifest system
- `STArray.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!), `DeliveredAmount`; serialization order is consensus-critical
- `LedgerHeader.h/cpp` 118-byte fixed serialization; close-time-resolution fudging
### Indexes and Keys
- `Indexes.h/cpp` `keylet::*` factories with `LedgerNameSpace` tagged hashing; `keylet::quality()` embeds 64-bit quality in last 8 bytes (big-endian)
- `Keylet.h/cpp` type-tagged `(uint256, LedgerEntryType)`; `ltANY` wildcard, `ltCHILD` rejects directories
- NFT pages: composite keys (high 160 = owner AccountID, low 96 = token range); `nft::pageMask` is the boundary
- `Protocol.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); LCG `384160001 * seq + 2459` ciphers taxon
- `NFTokenOfferID.h/cpp`, `NFTSyntheticSerializer.h/cpp` derived/synthetic NFT entries
- `Book.h` `(in_asset, out_asset)` order-book identity
- `SeqProxy.h/cpp` sequence vs ticket abstraction
### Validation Helpers (return NotTEC, preflight-time)
- `AMMCore.h/cpp` `invalidAMMAsset`, `invalidAMMAssetPair`, `invalidAMMAmount`; `ammLPTCurrency()` uses canonical `std::minmax`
- `Permissions.h/cpp` singleton; `isDelegable()` checks granular vs transaction-level (`<UINT16_MAX` boundary), amendment, delegable flag
### RPC/JSON Boundary
### RPC / JSON Boundary
- `STParsedJSON.h/cpp` depth cap 64; field-path-qualified errors via `make_name`; recognizes `"Payment"`, `"tesSUCCESS"`, etc.
- `ErrorCodes.h/cpp` append-only enum; `sortedErrorInfos` validated at compile time
- `MultiApiJson.h` per-API-version `Json::Value` array; composes with `forAllApiVersions` from `ApiVersion.h`
- `ErrorCodes.h/cpp` append-only enum; `sortedErrorInfos` validated at compile time; `warning_code_i` distinct from `error_code_i`
- `RPCErr.h/cpp` `RPC::Status`/`make_error` helpers
- `MultiApiJson.h` per-API-version `Json::Value` array indexed `[version - RPC::apiMinimumVersion]`; composes with `forAllApiVersions` from `ApiVersion.h`; preserves wire compatibility across versions
- `jss.h` every JSON key as `Json::StaticString` via `JSS(name)` macro; PascalCase = protocol fields, snake_case = RPC
- `json_get_or_throw.h` `getOrThrow<T>(jv, name)` specializations enforce presence + type; standard idiom for parsing untrusted JSON
- `st.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_out` proportional scaling; `_strict` variants honor Number rounding mode
- `QualityFunction.h/cpp` linear `q(out)=m*out+b`; AMMTag (slope from pool) vs CLOBLikeTag (m=0); `combine()` for multi-step strands
- `XChainAttestations.h/cpp` Attestations:: namespace (full, with signature) vs xrpl:: (stored); `match()` returns three-state `AttestationMatch`
- `XChainAttestations.h/cpp` `Attestations::` namespace (full, with signature) vs `xrpl::` (stored); `match()` returns three-state `AttestationMatch`
### Pseudo-Account Fields (sMD_PseudoAccount)
### 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/handshake
- `SystemParameters.h` drops-per-XRP, `INITIAL_XRP`, ledger-related constants; validated by `static_assert`
- `UintTypes.h` `uint256`/`uint160`/`uint128` aliases and tagged variants (`Currency`, `NodeID`, etc.)
- `TER.h/cpp` error code enum families + `TERSubset`
- `TxFlags.h` X-macro driven flag tables (`tf*`); see TxFlags Architecture below
- `TxFormats.h/cpp` transaction-type field schema
## Numeric Encoding Reference
```
@@ -253,6 +321,7 @@ 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 (050000), convert via nft::transferFeeAsRate (×10000)
AMM auction fee: basis points; trading fee in tenths of basis points (10000 = 1%)
```
## Protocol-Stable Constants (NEVER CHANGE)
@@ -262,10 +331,26 @@ NFT transfer fee: uint16 basis points (050000), convert via nft::transferFee
- `SerializedTypeID` and `SField` codes (in serialized fields)
- `LedgerNameSpace` discriminator characters (in keylet derivation)
- `HashPrefix` enum values (in signature/hash domain separation)
- `error_code_i` numeric values (clients depend on them; append-only)
- `error_code_i` and `warning_code_i` numeric values (clients depend on them; append-only)
- `TECcodes` (and other `TER` family numeric values) recorded in transaction metadata
- `TokenType` (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 by `static_assert` against `Number::maxRep`)
- NFT taxon LCG constants (`384160001 * seq + 2459`)
- All flag bit values (`tf*`, `lsf*`, `asf*`)
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
- `tfUniversalMask` is the union of universal flags (`tfFullyCanonicalSig`, `tfInnerBatchTxn`)
- Per-transaction `tf*Mask` constants are auto-computed via `MASK_ADJ` so that mask matches the declared flags exactly adding a flag automatically updates the mask
- `TF_FLAG2` marks flags whose meaning was changed by an amendment; old/new bits coexist with disjoint enable conditions
- Inner-batch flag `tfInnerBatchTxn` is 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.