Files
rippled/include/xrpl/protocol/STValidation.h
Denis Angell 88794a1ea9 docs: add Doxygen comments across xrpl and xrpld
Bulk documentation pass covering 702 C++ source files in src/libxrpl, src/xrpld, and
include/xrpl. Adds class, function, parameter, and invariant docs per
docs/DOCUMENTATION_STANDARDS.md.

Squashed from the original three-part series (part 1 / part 2 / part 3) to avoid
merge-conflict noise when rebasing the work onto current develop.
2026-05-14 10:20:15 +02:00

457 lines
17 KiB
C++

/** @file
* Defines `STValidation`, the wire-format object for a single ledger
* validation message in the XRPL consensus protocol.
*
* Validators broadcast one of these objects each consensus round to signal
* agreement on a specific closed ledger. Peers deserialize inbound messages
* into `STValidation` instances, verify signatures, and count them toward
* quorum. The class therefore has two distinct construction paths: one for
* creation-and-signing by the local validator, one for deserialization of a
* peer's message. See the two constructors for details.
*
* `STValidation` is owned via `std::shared_ptr` and wrapped by `RCLValidation`
* in the consensus machinery; that adapter provides the concept interface
* expected by the generic quorum-counting engine without coupling this class
* to consensus-specific logic.
*/
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Units.h>
#include <cstdint>
#include <optional>
#include <sstream>
namespace xrpl {
// --- Wire flag constants (stored in sfFlags; part of the signed payload) ---
/** Bit flag indicating a full (as opposed to a partial) validation.
*
* A partial validation signals participation in the consensus round without
* fully endorsing a specific ledger hash. Validators set this flag when they
* have applied the consensus transaction set and validated the resulting
* ledger. Read via `isFull()`.
*/
constexpr std::uint32_t kVF_FULL_VALIDATION = 0x00000001;
/** Bit flag indicating that the DER-encoded signature uses the low-S canonical form.
*
* XRPL requires low-S ECDSA signatures to prevent signature malleability.
* The signing constructor always sets this flag, and `isValid()` passes it
* to `verifyDigest()` to enforce canonicality on inbound messages. Because
* this value is stored in `sfFlags` inside the signed payload, it cannot be
* toggled without invalidating the signature.
*/
constexpr std::uint32_t kVF_FULLY_CANONICAL_SIG = 0x80000000;
/** Wire-format representation of a ledger validation message in XRPL consensus.
*
* Inherits from `STObject` for typed-field serialization (the same system
* used by transactions and ledger entries) and from `CountedObject` for
* live-instance tracking in a long-running `rippled` process.
*
* The class maintains two separate concepts that must not be conflated:
* - **Validity** (`valid_`): whether the cryptographic signature is correct.
* Lazily evaluated and cached on first call to `isValid()`.
* - **Trust** (`trusted_`): whether the issuing validator is on this node's
* current Unique Node List (UNL). Set via `setTrusted()`/`setUntrusted()`;
* can change at runtime as the UNL evolves.
*
* @note Only `secp256k1` signing keys are accepted. Passing an `Ed25519`
* public key to either constructor throws at construction time.
* @see RCLValidation — the adapter that exposes this object to the generic
* consensus engine.
*/
class STValidation final : public STObject, public CountedObject<STValidation>
{
bool trusted_ = false;
// Determines the validity of the signature in this validation; unseated
// optional if we haven't yet checked it, a boolean otherwise.
mutable std::optional<bool> valid_;
// The public key associated with the key used to sign this validation
PublicKey const signingPubKey_;
// The ID of the validator that issued this validation. For validators
// that use manifests this will be derived from the master public key.
NodeID const nodeID_;
NetClock::time_point seenTime_;
public:
/** Deserialize a validation received from a peer.
*
* Parses the binary payload via `STObject`, then extracts the signing
* public key from `sfSigningPubKey`. The `lookupNodeID` callable
* translates the ephemeral signing key to the validator's stable master
* `NodeID` (which may differ when the validator has rotated its ephemeral
* key via the manifest mechanism).
*
* @tparam LookupNodeID Callable with signature `NodeID(PublicKey const&)`.
* For manifest-based validators this should resolve to the master key's
* `NodeID`; for static-key validators it is typically
* `calcNodeID(pk)`.
* @param sit Iterator over the raw serialized validation bytes.
* @param lookupNodeID Invocable that maps the signing `PublicKey` to a
* stable `NodeID` used for UNL membership checks.
* @param checkSignature If `true`, verifies the signature immediately and
* throws on failure. Pass `false` to defer verification to the first
* call of `isValid()` (the pattern used by `PeerImp` to avoid
* synchronous cryptographic work on the peer-message path).
* @throws std::runtime_error if the serialized data is malformed, if the
* signing public key is absent or not a `secp256k1` key, or if
* `checkSignature` is `true` and the signature does not verify.
* @note After construction `seenTime_` is zero; callers must call
* `setSeen()` to record local receipt time before storing the object.
*/
template <class LookupNodeID>
STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature);
/** Construct, sign, and trust a new validation issued by the local node.
*
* Sets mandatory bookkeeping fields (`sfSigningPubKey`, `sfSigningTime`),
* invokes the filler callback `f(*this)` so the caller can attach
* optional fields (ledger hash, consensus hash, fee votes, amendment
* bits, server version), then signs the result with `signDigest` and
* marks the object as trusted. The `kVF_FULLY_CANONICAL_SIG` flag is
* always set, enforcing low-S ECDSA on the produced signature.
*
* After `f` returns a format-validation sweep checks that all
* `SoeRequired` fields are present; a missing required field is a
* programming error and triggers `logicError`.
*
* `seenTime_` is initialized to `signTime`, making sign time and seen
* time identical for locally created validations.
*
* @tparam F Callable with signature `void(STValidation&)`.
* @param signTime The time at which the validation is being signed;
* stored in `sfSigningTime` and used as the initial `seenTime_`.
* @param pk The validator's current ephemeral signing public key.
* Must be a `secp256k1` key; passing any other type calls
* `logicError`.
* @param sk The secret key matching `pk`, used to produce `sfSignature`.
* @param nodeID The stable master-key `NodeID` of this validator.
* @param f Callback invoked after mandatory fields are set but before
* signing. Use it to populate `sfLedgerHash`, `sfLedgerSequence`,
* `sfConsensusHash`, `sfFlags`, and any optional advisory fields.
* @note The resulting object is immediately marked as trusted and
* `valid_` is set to `true` without re-verifying, since the node
* just produced the signature.
*/
template <typename F>
STValidation(
NetClock::time_point signTime,
PublicKey const& pk,
SecretKey const& sk,
NodeID const& nodeID,
F&& f);
/** Return the hash of the ledger this validation endorses.
*
* @return Value of the `sfLedgerHash` field.
*/
uint256
getLedgerHash() const;
/** Return the hash of the consensus transaction set that produced the validated ledger.
*
* @return Value of the `sfConsensusHash` field. Returns a zero hash if
* the field is absent (the field is optional in the schema).
*/
uint256
getConsensusHash() const;
/** Return the time at which the validator claims to have signed this validation.
*
* Reads `sfSigningTime` from the serialized payload; because that field
* is part of the signed content it cannot be forged without invalidating
* the signature. Note that this is the validator's own clock time and
* may differ from `getSeenTime()`, which is when the *local* node
* received the message.
*
* @return The signing instant as a `NetClock::time_point`.
*/
NetClock::time_point
getSignTime() const;
/** Return the local time at which this node received or created the validation.
*
* For peer-sourced validations this is set via `setSeen()` after receipt.
* For self-issued validations the constructor initializes it to `signTime`.
* This value is never serialized or sent over the wire.
*
* @return The local receipt time as a `NetClock::time_point`.
*/
NetClock::time_point
getSeenTime() const noexcept;
/** Return the ephemeral public key that signed this validation.
*
* May differ from the validator's stable master key when the validator
* has rotated its signing key via the manifest mechanism.
*
* @return A reference to the immutable `signingPubKey_`.
*/
PublicKey const&
getSignerPublic() const noexcept;
/** Return the stable master-key `NodeID` of the issuing validator.
*
* For manifest-based validators this is derived from the master public
* key rather than the ephemeral signing key. It is the identity used
* for UNL membership checks and quorum counting.
*
* @return A reference to the immutable `nodeID_`.
*/
NodeID const&
getNodeID() const noexcept;
/** Verify the cryptographic signature, caching the result for future calls.
*
* On the first call, verifies the ECDSA signature over `getSigningHash()`
* using `signingPubKey_`. The `kVF_FULLY_CANONICAL_SIG` flag is consulted
* to enforce low-S canonicality. The result is stored in `valid_` and
* returned on all subsequent calls without re-computing.
*
* For self-issued validations the signing constructor pre-sets
* `valid_ = true`, so this method never performs cryptographic work.
*
* @return `true` if the signature is valid; `false` otherwise.
*/
bool
isValid() const noexcept;
/** Return whether this is a full (as opposed to partial) validation.
*
* A full validation endorses a specific ledger hash. A partial validation
* only signals that the validator participated in the round.
*
* @return `true` if the `kVF_FULL_VALIDATION` bit is set in `sfFlags`.
*/
bool
isFull() const noexcept;
/** Return whether this validation is marked as trusted by the local node.
*
* Trust reflects whether the issuing validator is on this node's current
* UNL and is independent of cryptographic validity. Self-issued
* validations are always trusted from construction.
*
* @return The current value of the `trusted_` flag.
*/
bool
isTrusted() const noexcept;
/** Compute the domain-separated hash that was (or will be) signed.
*
* Prepends `HashPrefix::Validation` (`'V','A','L',0x00`) to the canonical
* serialization of all signed fields, then applies SHA-512-Half. The
* prefix prevents a validation hash from colliding with any other signed
* payload type (transactions, proposals, etc.).
*
* @return The 256-bit signing digest.
*/
uint256
getSigningHash() const;
/** Mark this validation as trusted.
*
* Called when the issuing validator is confirmed to be on this node's
* current UNL. May be called multiple times; subsequent calls are no-ops.
*/
void
setTrusted();
/** Mark this validation as untrusted.
*
* Called when the issuing validator is removed from this node's current
* UNL, or when the validation is being re-evaluated. Does not affect the
* cryptographic `valid_` cache.
*/
void
setUntrusted();
/** Record the local time at which this node received the validation.
*
* Should be called immediately after constructing a peer-sourced
* validation, before the object is stored or forwarded. For self-issued
* validations the signing constructor sets this to `signTime`
* automatically.
*
* @param s The local receipt time.
*/
void
setSeen(NetClock::time_point s);
/** Serialize this validation to its complete binary wire format.
*
* The returned bytes include all fields, including `sfSignature`, and are
* suitable for network transmission or deduplication hashing. To suppress
* relay of a duplicate message, callers typically hash this output with
* `sha512Half`.
*
* @return A `Blob` containing the complete serialized validation.
*/
Blob
getSerialized() const;
/** Return the raw DER-encoded ECDSA signature from the serialized payload.
*
* @return Value of the `sfSignature` field as a `Blob`.
*/
Blob
getSignature() const;
/** Produce a human-readable summary of this validation for logging.
*
* Renders all major fields (ledger hash, consensus hash, sign/seen times,
* signer public key, node ID, validity, fullness, trust status, signing
* hash, and Base58-encoded public key) into a single-line string.
*
* @return A diagnostic string; not suitable for machine parsing or
* network transmission.
*/
std::string
render() const
{
std::stringstream ss;
ss << "validation: " << " ledger_hash: " << getLedgerHash()
<< " consensus_hash: " << getConsensusHash()
<< " sign_time: " << to_string(getSignTime())
<< " seen_time: " << to_string(getSeenTime())
<< " signer_public_key: " << getSignerPublic() << " node_id: " << getNodeID()
<< " is_valid: " << isValid() << " is_full: " << isFull()
<< " is_trusted: " << isTrusted() << " signing_hash: " << getSigningHash()
<< " base58: " << toBase58(TokenType::NodePublic, getSignerPublic());
return ss.str();
}
private:
/** Return the field schema for `STValidation` objects.
*
* Function-local static to guarantee that all `SField` singletons are
* initialized before the `SOTemplate` is constructed (C++ provides no
* cross-translation-unit initialization order for namespace-scope statics).
*/
static SOTemplate const&
validationFormat();
STBase*
copy(std::size_t n, void* buf) const override;
STBase*
move(std::size_t n, void* buf) override;
friend class detail::STVar;
};
template <class LookupNodeID>
STValidation::STValidation(SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature)
: STObject(validationFormat(), sit, sfValidation)
, signingPubKey_([this]() {
auto const spk = getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)) != KeyType::Secp256k1)
Throw<std::runtime_error>("Invalid public key in validation");
return PublicKey{makeSlice(spk)};
}())
, nodeID_(lookupNodeID(signingPubKey_))
{
if (checkSignature && !isValid())
{
JLOG(debugLog().error()) << "Invalid signature in validation: "
<< getJson(JsonOptions::Values::None);
Throw<std::runtime_error>("Invalid signature in validation");
}
XRPL_ASSERT(nodeID_.isNonZero(), "xrpl::STValidation::STValidation(SerialIter) : nonzero node");
}
template <typename F>
STValidation::STValidation(
NetClock::time_point signTime,
PublicKey const& pk,
SecretKey const& sk,
NodeID const& nodeID,
F&& f)
: STObject(validationFormat(), sfValidation)
, signingPubKey_(pk)
, nodeID_(nodeID)
, seenTime_(signTime)
{
XRPL_ASSERT(
nodeID_.isNonZero(),
"xrpl::STValidation::STValidation(PublicKey, SecretKey) : nonzero "
"node");
// First, set our own public key:
if (publicKeyType(pk) != KeyType::Secp256k1)
logicError("We can only use secp256k1 keys for signing validations");
setFieldVL(sfSigningPubKey, pk.slice());
setFieldU32(sfSigningTime, signTime.time_since_epoch().count());
// Perform additional initialization
f(*this);
// Finally, sign the validation and mark it as trusted:
setFlag(kVF_FULLY_CANONICAL_SIG);
setFieldVL(sfSignature, signDigest(pk, sk, getSigningHash()));
setTrusted();
// Check to ensure that all required fields are present.
for (auto const& e : validationFormat())
{
if (e.style() == SoeRequired && !isFieldPresent(e.sField()))
logicError("Required field '" + e.sField().getName() + "' missing from validation.");
}
// We just signed this, so it should be valid.
valid_ = true;
}
inline PublicKey const&
STValidation::getSignerPublic() const noexcept
{
return signingPubKey_;
}
inline NodeID const&
STValidation::getNodeID() const noexcept
{
return nodeID_;
}
inline bool
STValidation::isTrusted() const noexcept
{
return trusted_;
}
inline void
STValidation::setTrusted()
{
trusted_ = true;
}
inline void
STValidation::setUntrusted()
{
trusted_ = false;
}
inline void
STValidation::setSeen(NetClock::time_point s)
{
seenTime_ = s;
}
} // namespace xrpl