Files
rippled/include/xrpl/protocol/SField.h
Denis Angell e635557235 part 2
2026-05-14 05:56:04 +02:00

654 lines
24 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @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<T>` 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 <xrpl/basics/safe_cast.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/Units.h>
#include <cstdint>
#include <map>
namespace xrpl {
//------------------------------------------------------------------------------
// Forwards
class STAccount;
class STAmount;
class STIssue;
class STBlob;
template <int>
class STBitString;
template <class>
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 111 ("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 1213 are reserved gaps.
* Codes 1000110004 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<std::string, int> 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<int>(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<int, SField const*> const&
getKnownCodeToField()
{
return knownCodeToField;
}
private:
static int num;
static std::unordered_map<int, SField const*> knownCodeToField;
static std::unordered_map<std::string, SField const*> 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<STInteger<uint32_t>>`, 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<uint32_t>`).
*
* @see OptionaledField, operator~
*/
template <class T>
struct TypedField : SField
{
using type = T;
template <class... Args>
explicit TypedField(PrivateAccessTagT pat, Args&&... args);
};
/** Wrapper indicating that a `TypedField` may be absent in a given object.
*
* Obtained via `operator~(TypedField<T> const&)`. The `STObject` proxy
* access pattern uses this to return `std::optional<T>` instead of throwing
* when the field is missing.
*
* @tparam T The serialized C++ type of the underlying field.
*
* @see operator~
*/
template <class T>
struct OptionaledField
{
TypedField<T> const* f;
explicit OptionaledField(TypedField<T> 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<T>` that is empty when the field is absent.
*
* @param f The typed field to treat as optional.
* @return An `OptionaledField<T>` wrapping `f`.
*/
template <class T>
inline OptionaledField<T>
operator~(TypedField<T> const& f)
{
return OptionaledField<T>(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<STInteger<std::uint8_t>>;
using SF_UINT16 = TypedField<STInteger<std::uint16_t>>;
using SF_UINT32 = TypedField<STInteger<std::uint32_t>>;
using SF_UINT64 = TypedField<STInteger<std::uint64_t>>;
using SF_UINT96 = TypedField<STBitString<96>>;
using SF_UINT128 = TypedField<STBitString<128>>;
using SF_UINT160 = TypedField<STBitString<160>>;
using SF_UINT192 = TypedField<STBitString<192>>;
using SF_UINT256 = TypedField<STBitString<256>>;
using SF_UINT384 = TypedField<STBitString<384>>;
using SF_UINT512 = TypedField<STBitString<512>>;
using SF_INT32 = TypedField<STInteger<std::int32_t>>;
using SF_INT64 = TypedField<STInteger<std::int64_t>>;
using SF_ACCOUNT = TypedField<STAccount>;
using SF_AMOUNT = TypedField<STAmount>;
using SF_ISSUE = TypedField<STIssue>;
using SF_CURRENCY = TypedField<STCurrency>;
using SF_NUMBER = TypedField<STNumber>;
using SF_VL = TypedField<STBlob>;
using SF_VECTOR256 = TypedField<STVector256>;
using SF_XCHAIN_BRIDGE = TypedField<STXChainBridge>;
/** @} */
//------------------------------------------------------------------------------
// 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 <xrpl/protocol/detail/sfields.macro>
#undef TYPED_SFIELD
#pragma pop_macro("TYPED_SFIELD")
#undef UNTYPED_SFIELD
#pragma pop_macro("UNTYPED_SFIELD")
} // namespace xrpl