/** @file * Compile-time field identification and wire-type catalog for XRPL serialized * objects. * * Every data field that can appear in an XRPL transaction, ledger entry, * validation, or transaction metadata is identified by a singleton `SField` * instance declared in this header. The `SerializedTypeID` enum defines the * recognized wire types. `TypedField` adds a compile-time C++ type so * that callers can interact with fields in a type-safe way. * * @note Some fields distinguish between the default value and the absent * state. For example, `sfQualityIn` on a trust line with value 0 * means "no quality set" (absent) versus 1,000,000,000 (parity rate * when explicitly set). Keep this in mind when testing presence. * * @see SField, TypedField, SerializedTypeID */ #pragma once #include #include #include #include #include namespace xrpl { //------------------------------------------------------------------------------ // Forwards class STAccount; class STAmount; class STIssue; class STBlob; template class STBitString; template class STInteger; class STNumber; class STXChainBridge; class STVector256; class STCurrency; // NOLINTBEGIN(readability-identifier-naming) /** Wire-type codes for XRPL binary serialization. * * Each value identifies the on-the-wire encoding family used for a group of * protocol fields. Codes 1–11 ("common" types) fit in a single nibble and * share a compact one-byte field-ID prefix. Codes 16+ ("uncommon" types) * require an extra byte for the type nibble. Codes 12–13 are reserved gaps. * Codes 10001–10004 are top-level container types (`STI_TRANSACTION`, etc.) * that cannot be embedded inside other serialized objects. * * The enum and the companion string-to-int map `kS_TYPE_MAP` are both * generated from a single `XMACRO` expansion — adding a new type requires * only one line in the macro. * * @note These numeric values are protocol-stable: changing them would break * binary serialization compatibility with existing ledger data and peers. */ #pragma push_macro("XMACRO") #undef XMACRO #define XMACRO(STYPE) \ /* special types */ \ STYPE(STI_UNKNOWN, -2) \ STYPE(STI_NOTPRESENT, 0) \ STYPE(STI_UINT16, 1) \ \ /* types (common) */ \ STYPE(STI_UINT32, 2) \ STYPE(STI_UINT64, 3) \ STYPE(STI_UINT128, 4) \ STYPE(STI_UINT256, 5) \ STYPE(STI_AMOUNT, 6) \ STYPE(STI_VL, 7) \ STYPE(STI_ACCOUNT, 8) \ STYPE(STI_NUMBER, 9) \ STYPE(STI_INT32, 10) \ STYPE(STI_INT64, 11) \ \ /* 12-13 are reserved */ \ STYPE(STI_OBJECT, 14) \ STYPE(STI_ARRAY, 15) \ \ /* types (uncommon) */ \ STYPE(STI_UINT8, 16) \ STYPE(STI_UINT160, 17) \ STYPE(STI_PATHSET, 18) \ STYPE(STI_VECTOR256, 19) \ STYPE(STI_UINT96, 20) \ STYPE(STI_UINT192, 21) \ STYPE(STI_UINT384, 22) \ STYPE(STI_UINT512, 23) \ STYPE(STI_ISSUE, 24) \ STYPE(STI_XCHAIN_BRIDGE, 25) \ STYPE(STI_CURRENCY, 26) \ \ /* high-level types */ \ /* cannot be serialized inside other types */ \ STYPE(STI_TRANSACTION, 10001) \ STYPE(STI_LEDGERENTRY, 10002) \ STYPE(STI_VALIDATION, 10003) \ STYPE(STI_METADATA, 10004) #pragma push_macro("TO_ENUM") #undef TO_ENUM #pragma push_macro("TO_MAP") #undef TO_MAP #define TO_ENUM(name, value) name = (value), #define TO_MAP(name, value) {#name, value}, // Protocol infrastructure, 39+ files // NOLINTNEXTLINE(cppcoreguidelines-use-enum-class) enum SerializedTypeID { XMACRO(TO_ENUM) }; /** String-to-integer map of all `SerializedTypeID` values. * * Generated by the same `XMACRO` expansion as `SerializedTypeID`, so the two * are always in sync. Used to resolve type names arriving as text (e.g. from * JSON or RPC) to their integer wire codes. */ static std::map const kS_TYPE_MAP = {XMACRO(TO_MAP)}; #undef XMACRO #undef TO_ENUM #pragma pop_macro("XMACRO") #pragma pop_macro("TO_ENUM") #pragma pop_macro("TO_MAP") // NOLINTEND(readability-identifier-naming) /** Pack a `SerializedTypeID` and per-type index into a single field code. * * The resulting integer is the canonical sort key used for deterministic * binary serialization: the upper 16 bits hold the type family and the lower * 16 bits hold the field's position within that family. Fields are always * serialized in ascending `fieldCode` order. * * @param id The wire-type family (e.g. `STI_UINT32`). * @param index The per-type field index (e.g. 4 for `sfSequence`). * @return The packed field code `(id << 16) | index`. */ inline int fieldCode(SerializedTypeID id, int index) { return (safeCast(id) << 16) | index; } /** Pack a raw integer type ID and per-type index into a single field code. * * Overload for callers that already have the type as a plain `int` (e.g. * when deserializing an unknown type from the wire). * * @param id The wire-type family as a raw integer. * @param index The per-type field index. * @return The packed field code `(id << 16) | index`. */ inline int fieldCode(int id, int index) { return (id << 16) | index; } /** Identifies a single named field in XRPL's binary serialization protocol. * * Every field that can appear in a transaction, ledger entry, validation, or * transaction metadata is represented by exactly one `SField` singleton. All * instances are created at static-initialization time in `SField.cpp` and * live until program termination; copy, move, and assignment are deleted to * enforce the singleton guarantee. * * Each field carries a packed `fieldCode` (`(SerializedTypeID << 16) | * fieldValue`) that serves as both the registry key and the canonical * comparison value for determining binary serialization order. Fields are * always serialized in ascending `fieldCode` order — required for * deterministic transaction signing. * * Construction is restricted to `SField.cpp` via `PrivateAccessTagT`: the * tag type is forward-declared public here but defined only in that * translation unit, so external code can only look up existing fields through * `getField()`. * * @note Debug builds assert that no two fields share the same code or name at * registration time. Release builds do not check; a duplicate would * silently shadow the earlier field. * * @see TypedField, fieldCode(), SerializedTypeID */ class SField { public: /** Never capture this field's value in transaction metadata. */ static constexpr auto kSMD_NEVER = 0x00; /** Capture the original value when the field changes. */ static constexpr auto kSMD_CHANGE_ORIG = 0x01; /** Capture the new value when the field changes. */ static constexpr auto kSMD_CHANGE_NEW = 0x02; /** Capture the final value when the enclosing object is deleted. */ static constexpr auto kSMD_DELETE_FINAL = 0x04; /** Capture the value when the enclosing object is first created. */ static constexpr auto kSMD_CREATE = 0x08; /** Capture the value whenever the enclosing ledger node is touched, * regardless of whether the field itself changed (used by `sfRootIndex`). */ static constexpr auto kSMD_ALWAYS = 0x10; /** Display the value in base-10 rather than hex in JSON metadata * (used by MPT amount fields such as `sfMaximumAmount`). */ static constexpr auto kSMD_BASE_TEN = 0x20; /** The field holds a 256-bit hash that identifies a pseudo-account * (AMM, Vault, LoanBroker). Used by `sfAMMID`, `sfVaultID`, * `sfLoanBrokerID`. */ static constexpr auto kSMD_PSEUDO_ACCOUNT = 0x40; /** The field is an `STNumber` that must have `associateAsset()` called * before the enclosing ledger object is serialized. The association * rounds the value to the asset's precision and removes it if it becomes * zero (pairs with `kSMD_DEFAULT`). */ static constexpr auto kSMD_NEEDS_ASSET = 0x80; /** Default metadata flags: record original value, new value, deletion * value, and creation value (`kSMD_CHANGE_ORIG | kSMD_CHANGE_NEW | * kSMD_DELETE_FINAL | kSMD_CREATE`). */ static constexpr auto kSMD_DEFAULT = kSMD_CHANGE_ORIG | kSMD_CHANGE_NEW | kSMD_DELETE_FINAL | kSMD_CREATE; /** Controls whether a field is included in a transaction's signing payload. * * Fields that carry signatures (`sfTxnSignature`, `sfSigners`, * `sfMasterSignature`, `sfSignature`, `sfCounterpartySignature`) are * marked `No` to prevent the bootstrap paradox of a signature covering * itself. */ enum class IsSigning : unsigned char { No, Yes }; /** Convenience constant for the non-signing value. */ static IsSigning const kNOT_SIGNING = IsSigning::No; /** Packed field code: `(SerializedTypeID << 16) | fieldValue`. * This is the canonical sort key for binary serialization order. * Sentinel values: -1 for `kSF_INVALID`, 0 for `kSF_GENERIC`. */ int const fieldCodeMem; /** Wire-type family for this field (e.g. `STI_UINT32`). */ SerializedTypeID const fieldType; /** Per-type field index. Values < 256 are binary-serializable; * values > 256 are JSON-only (discardable). */ int const fieldValue; /** Human-readable field name without the `sf` prefix (e.g. `"Sequence"`). */ std::string const fieldName; /** Bitmask of `kSMD_*` flags controlling transaction metadata capture. */ int const fieldMeta; /** Monotonically increasing registration ordinal (1-based). */ int const fieldNum; /** Whether this field is included in the signing payload. */ IsSigning const signingField; /** JSON key for this field as a `StaticString` (pointer-stable). */ json::StaticString const jsonName; SField(SField const&) = delete; SField& operator=(SField const&) = delete; SField(SField&&) = delete; SField& operator=(SField&&) = delete; public: /** Construction access guard — public type, private definition. * * Forward-declared here so the constructor signatures are visible, but * the struct body (and its constructor) is defined only in `SField.cpp`. * Consequently, only `SField.cpp` can construct `SField` instances. */ struct PrivateAccessTagT; /** Construct a typed, named protocol field and register it globally. * * Computes `fieldCode = (tid << 16) | fv` and inserts this field into * the `knownCodeToField` and `knownNameToField` lookup tables. Only * callable from `SField.cpp` (enforced by `PrivateAccessTagT`). * * @param tid Serialized type family (e.g. `STI_UINT32`). * @param fv Per-type field index; must be < 256 to be * binary-serializable. * @param fn Human-readable field name (`sf` prefix already stripped * by the calling macro). * @param meta Bitmask of `kSMD_*` flags; defaults to `kSMD_DEFAULT`. * @param signing Whether this field appears in signing payloads; defaults * to `IsSigning::Yes`. */ SField( PrivateAccessTagT, SerializedTypeID tid, int fv, char const* fn, int meta = kSMD_DEFAULT, IsSigning signing = IsSigning::Yes); /** Construct a special-purpose field from a raw field code. * * Used only for the four historical outlier fields (`kSF_INVALID`, * `kSF_GENERIC`, `kSF_HASH`, `kSF_INDEX`) whose codes cannot be derived * from the standard `(tid << 16) | fv` formula. Sets `fieldType` to * `STI_UNKNOWN` and `fieldMeta` to `kSMD_NEVER`. * * @param fc Raw field code; -1 for `kSF_INVALID`, 0 for `kSF_GENERIC`. * @param fn Human-readable field name. */ explicit SField(PrivateAccessTagT, int fc, char const* fn); /** Look up a registered field by its packed field code. * * @param fieldCode Packed code `(SerializedTypeID << 16) | fieldValue`. * @return The matching `SField`, or `kSF_INVALID` if none is registered * with that code. */ static SField const& getField(int fieldCode); /** Look up a registered field by its human-readable name. * * Names are stored without the `sf` prefix (e.g. `"Sequence"` not * `"sfSequence"`). * * @param fieldName The name to search for (no `sf` prefix). * @return The matching `SField`, or `kSF_INVALID` if none is registered * with that name. */ static SField const& getField(std::string const& fieldName); /** Look up a registered field by raw integer type ID and field index. * * @param type Wire-type family as a raw integer. * @param value Per-type field index. * @return The matching `SField`, or `kSF_INVALID` if not found. */ static SField const& getField(int type, int value) { return getField(fieldCode(type, value)); } /** Look up a registered field by `SerializedTypeID` and field index. * * @param type Wire-type family. * @param value Per-type field index. * @return The matching `SField`, or `kSF_INVALID` if not found. */ static SField const& getField(SerializedTypeID type, int value) { return getField(fieldCode(type, value)); } /** Return the human-readable field name (without the `sf` prefix). */ [[nodiscard]] std::string const& getName() const { return fieldName; } /** Return true if this field has a meaningful name and positive field code. * * Returns false for `kSF_INVALID` (`fieldCode == -1`) and `kSF_GENERIC` * (`fieldCode == 0`). */ [[nodiscard]] bool hasName() const { return fieldCodeMem > 0; } /** Return the JSON key for this field as a pointer-stable `StaticString`. */ [[nodiscard]] json::StaticString const& getJsonName() const { return jsonName; } /** Implicit conversion to `json::StaticString` for use as a JSON key. */ operator json::StaticString const&() const { return jsonName; } /** Return true if this field is the `kSF_INVALID` sentinel (`fieldCode == -1`). * * `getField()` returns `kSF_INVALID` on a lookup miss. */ [[nodiscard]] bool isInvalid() const { return fieldCodeMem == -1; } /** Return true if this field has a positive field code and can carry data. * * Equivalent to `!isInvalid() && hasName()`; false for `kSF_INVALID` and * `kSF_GENERIC`. */ [[nodiscard]] bool isUseful() const { return fieldCodeMem > 0; } /** Return true if this field can be round-tripped through binary serialization. * * A field is binary-serializable when `fieldValue < 256`. Fields with * `fieldValue >= 256` (e.g. `kSF_HASH`, `kSF_INDEX`) exist only in JSON * representations and are excluded from binary encoding. */ [[nodiscard]] bool isBinary() const { return fieldValue < 256; } /** Return true if this field must be silently dropped during binary serialization. * * Discardable fields (e.g. `sfHash`, `sfIndex`) have `fieldValue > 256` * and exist only in the JSON form of an object. A round-trip through * binary will lose them. */ [[nodiscard]] bool isDiscardable() const { return fieldValue > 256; } /** Return the packed field code `(SerializedTypeID << 16) | fieldValue`. */ [[nodiscard]] int getCode() const { return fieldCodeMem; } /** Return the 1-based registration ordinal assigned at static-init time. */ [[nodiscard]] int getNum() const { return fieldNum; } /** Return the total number of `SField` instances registered so far. */ static int getNumFields() { return num; } /** Return true if any of the bits in `c` are set in this field's metadata mask. * * @param c A bitmask of one or more `kSMD_*` constants. */ [[nodiscard]] bool shouldMeta(int c) const { return (fieldMeta & c) != 0; } /** Return true if this field should be included in a serialization pass. * * A field is included when it is binary-serializable (`fieldValue < 256`) * and either the caller wants all fields (`withSigningField == true`) or * this field is marked `IsSigning::Yes`. Passing `withSigningField == * false` excludes non-signing fields (used when building the signing * payload for a transaction). * * @param withSigningField If false, fields marked `IsSigning::No` are * excluded. */ [[nodiscard]] bool shouldInclude(bool withSigningField) const { return (fieldValue < 256) && (withSigningField || (signingField == IsSigning::Yes)); } /** Equality based on packed field code. */ bool operator==(SField const& f) const { return fieldCodeMem == f.fieldCodeMem; } /** Inequality based on packed field code. */ bool operator!=(SField const& f) const { return fieldCodeMem != f.fieldCodeMem; } /** Compare two fields by canonical binary-serialization order. * * Fields are ordered by `fieldCode = (SerializedTypeID << 16) | * fieldValue`, sorting first by wire-type family and then by per-type * index — matching the canonical XRPL binary format required for * deterministic transaction signing. * * @param f1 First field. * @param f2 Second field. * @return -1 if `f1` precedes `f2`, 1 if `f1` follows `f2`, or 0 if * the comparison is illegal because either field has a non-positive * code (`kSF_INVALID` or `kSF_GENERIC`). */ static int compare(SField const& f1, SField const& f2); /** Return a read-only reference to the global code-to-field registry. * * The map key is the packed field code `(SerializedTypeID << 16) | * fieldValue`. Intended for diagnostic and introspection use only; * prefer `getField()` for ordinary lookups. */ static std::unordered_map const& getKnownCodeToField() { return knownCodeToField; } private: static int num; static std::unordered_map knownCodeToField; static std::unordered_map knownNameToField; }; /** An `SField` whose associated C++ type is known at compile time. * * Extends `SField` with a `type` alias so callers can statically verify that * a field is read or written with the correct serialized C++ type. For * example, `SF_UINT32` is `TypedField>`, making it a * compile error to read it as an `STAmount`. * * All `TypedField` instances are singletons constructed in `SField.cpp`; * external code cannot create new instances. * * @tparam T The serialized C++ type for this field (e.g. `STAmount`, * `STInteger`). * * @see OptionaledField, operator~ */ template struct TypedField : SField { using type = T; template explicit TypedField(PrivateAccessTagT pat, Args&&... args); }; /** Wrapper indicating that a `TypedField` may be absent in a given object. * * Obtained via `operator~(TypedField const&)`. The `STObject` proxy * access pattern uses this to return `std::optional` instead of throwing * when the field is missing. * * @tparam T The serialized C++ type of the underlying field. * * @see operator~ */ template struct OptionaledField { TypedField const* f; explicit OptionaledField(TypedField const& f) : f(&f) { } }; /** Construct an `OptionaledField` from a `TypedField`, expressing optional semantics. * * Allows callers to write `~sfAmount` instead of `OptionaledField(sfAmount)`. * The resulting value is used with the `STObject` proxy access API to obtain * an `std::optional` that is empty when the field is absent. * * @param f The typed field to treat as optional. * @return An `OptionaledField` wrapping `f`. */ template inline OptionaledField operator~(TypedField const& f) { return OptionaledField(f); } //------------------------------------------------------------------------------ /** @defgroup SFieldTypeAliases Typed SField aliases * Convenience type aliases pairing each `SerializedTypeID` wire family with * its C++ serialized type. Use these as the type of `extern` field * declarations so that the field carries full type information at compile * time. * @{ */ using SF_UINT8 = TypedField>; using SF_UINT16 = TypedField>; using SF_UINT32 = TypedField>; using SF_UINT64 = TypedField>; using SF_UINT96 = TypedField>; using SF_UINT128 = TypedField>; using SF_UINT160 = TypedField>; using SF_UINT192 = TypedField>; using SF_UINT256 = TypedField>; using SF_UINT384 = TypedField>; using SF_UINT512 = TypedField>; using SF_INT32 = TypedField>; using SF_INT64 = TypedField>; using SF_ACCOUNT = TypedField; using SF_AMOUNT = TypedField; using SF_ISSUE = TypedField; using SF_CURRENCY = TypedField; using SF_NUMBER = TypedField; using SF_VL = TypedField; using SF_VECTOR256 = TypedField; using SF_XCHAIN_BRIDGE = TypedField; /** @} */ //------------------------------------------------------------------------------ // Use macros for most SField construction to enforce naming conventions. #pragma push_macro("UNTYPED_SFIELD") #undef UNTYPED_SFIELD #pragma push_macro("TYPED_SFIELD") #undef TYPED_SFIELD #define UNTYPED_SFIELD(sfName, stiSuffix, fieldValue, ...) extern SField const sfName; #define TYPED_SFIELD(sfName, stiSuffix, fieldValue, ...) extern SF_##stiSuffix const sfName; /** Sentinel returned by `SField::getField()` on a lookup miss. * * `fieldCode == -1`; `isInvalid()` returns true. Callers that receive this * value should treat the requested field as unrecognized. */ extern SField const kSF_INVALID; /** Catch-all field for untyped serialization contexts. * * `fieldCode == 0`; `isUseful()` and `hasName()` return false. Used * internally when a context requires an `SField` reference but no specific * field is applicable. */ extern SField const kSF_GENERIC; #include #undef TYPED_SFIELD #pragma pop_macro("TYPED_SFIELD") #undef UNTYPED_SFIELD #pragma pop_macro("UNTYPED_SFIELD") } // namespace xrpl