diff --git a/docs/skills/cryptography.md b/docs/skills/cryptography.md index f0e2881ac2..f7bd36ed2e 100644 --- a/docs/skills/cryptography.md +++ b/docs/skills/cryptography.md @@ -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 0–570 are 1–3 char words, 571–2047 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 2–3 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 64–65 -- `etob` validates: exactly 6 words, each 1–4 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 1–4 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 1–3 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 diff --git a/docs/skills/protocol.md b/docs/skills/protocol.md index 76ccd24233..5deb9a2f72 100644 --- a/docs/skills/protocol.md +++ b/docs/skills/protocol.md @@ -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` — pathfinding-only asset reference (no issuer); used inside `STPathElement`. + Conversion utilities in `AmountConversions.h`: `toSTAmount`, `toAmount`, `getAsset`. Lean→STAmount is implicit-friendly; STAmount→lean is explicit (`get<>` throws on type mismatch). +`Units.h` provides phantom-typed `ValueUnit` (`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`, `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` (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(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` (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 (0–192), 2 bytes (193–12480), 3 bytes (12481–918744); 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`; 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` 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` (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` with `forward_list` (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 hex→base58→RFC1751→passphrase, 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 (10–15× speedup, gated on non-MSVC for `__int128`) +- `tokens.h/cpp` (+ `b58_utils.h`, `token_errors.h`) — Base58Check; fast path uses base 58^10 intermediate (10–15× 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 (`(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`, `STInteger`, `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 (0–50000), 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 (0–50000), 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.