Merge branch 'pratik/otel-phase3-tx-tracing' into pratik/otel-phase4-consensus-tracing

Conflict in cmake/XrplCore.cmake: kept phase-4's comment block and applied
phase-3's two-line note about the SField table.
This commit is contained in:
Pratik Mankawde
2026-09-23 18:02:45 +01:00
17 changed files with 621 additions and 161 deletions

View File

@@ -1,34 +1,30 @@
#pragma once
/**
* Account-address redaction for telemetry span attributes.
* Account-address redaction helper for telemetry span attributes.
*
* Path-finding RPC handlers would otherwise emit the caller's raw
* account addresses as span attributes. To keep plaintext addresses out
* of the telemetry backend, they are hashed at the point of emission.
* This header exposes a single pure helper that turns an address into a
* short, stable, obfuscated token.
* A single pure helper that turns a string into a short, stable,
* obfuscated token, for a span attribute whose value should not be stored
* in the clear and is hard to guess.
*
* Data flow:
*
* handler -> redactAccount(addr) -> span attribute -> OTLP export
* Not applied to any span today. Account addresses are public ledger
* identifiers, so the path-finding spans emit them raw (see
* PathFindSpanNames.h) and no collector processor hashes them. Use this
* helper only for a value that is genuinely private, and document the
* reason at the attribute constant.
*
* The returned token is the first 16 hex characters (lowercase) of the
* SHA-512Half digest of the address. It is deterministic (same address
* always maps to the same token) so operators can still correlate spans
* for a given account across nodes and restarts.
* SHA-512Half digest of the input. It is deterministic (same input
* always maps to the same token) so spans for one value still correlate
* across nodes and restarts.
*
* The hash is unsalted, so it is obfuscation, not a secrecy guarantee:
* XRP account addresses are a public, enumerable set, so a determined
* observer with the telemetry stream could rebuild the address->token
* mapping. The goal here is to keep plaintext addresses out of traces
* and dashboards, not to defend against a precomputation attack. A salt
* is intentionally omitted because it would break cross-node/restart
* correlation, which is the reason for hashing rather than dropping.
*
* A second, independent hashing layer runs in the OpenTelemetry
* Collector (an `attributes/hash` processor) as defense-in-depth for
* any node that emits a raw value.
* The hash is unsalted, so it is obfuscation, not a secrecy guarantee.
* It hides a value only when that value is hard to guess: for an input
* drawn from a small or enumerable set, such as an account address, an
* observer can rebuild the value->token mapping by lookup, which is why
* account addresses are emitted raw instead. A salt is intentionally
* omitted because it would break cross-node/restart correlation, which is
* the reason for hashing rather than dropping.
*
* @note This function is pure and reentrant: it holds no global state,
* performs no I/O, and is safe to call concurrently from any thread.
@@ -38,8 +34,7 @@
* #include <xrpl/telemetry/Redaction.h>
* using namespace xrpl::telemetry;
*
* span.setAttribute(
* pathfind_span::attr::sourceAccount, redactAccount(src.asString()));
* auto const token = redactAccount(value); // 16 lowercase hex chars
* @endcode
*
* Edge case (empty input yields empty output):
@@ -54,9 +49,10 @@
namespace xrpl::telemetry {
/**
* Hash an account address into a short, stable, obfuscated token.
* Hash a value into a short, stable, obfuscated token.
*
* @param addr The account address to redact (e.g. an r-address).
* @param addr The value to redact. Named for its original use on account
* addresses; any string can be passed.
* @return The first 16 lowercase hex characters of sha512Half(addr),
* or an empty string when @p addr is empty.
*/

View File

@@ -0,0 +1,172 @@
#pragma once
/**
* Span attribute keys for the account-typed fields of a transaction.
*
* A transaction names one or more accounts: the sender in `Account`, and
* depending on the type a `Destination`, `Owner`, `Issuer`, `Holder` and so
* on. The tx.process span emits every one it finds as its own attribute, so
* an account can be searched for in traces whatever role it played. An
* account address is a public ledger identifier, so each is emitted as the
* raw r-address and never hashed.
*
* One key per protocol field: `tx_` followed by the field's JSON name in
* lower snake case. The full table is the initializer in
* src/libxrpl/telemetry/TxAccountSpanNames.cpp; the common ones are
*
* STTx field span attribute key
* ----------------- ------------------
* Account tx_account
* Destination tx_destination
* Owner tx_owner
* Issuer tx_issuer
* RegularKey tx_regular_key
* NFTokenMinter tx_nftoken_minter
*
* Only fields that some transaction format carries at top level have a key.
* Account-typed fields that appear only in ledger entries or inner objects
* (LowSponsor, LockingChainDoor, ...) map to nullopt. A library test walks
* TxFormats and fails when a format gains an account field with no key.
*
* Why this header lives in libxrpl rather than beside TxSpanNames.h: the
* mapping is keyed by protocol fields and its completeness is checked from
* TxFormats, which a library test can reach and a daemon header cannot.
*
* Data flow:
*
* NetworkOPs::processTransaction (src/xrpld)
* │ for each top-level field with getSType() == STI_ACCOUNT
* ▼
* accountFieldAttributeKey(field.getFName()) (this header)
* │ the key, or nullopt for a field with no key
* ▼
* span->setAttribute(key, field.getText())
*
* @code
* // Primary use: emit every account the transaction names. An empty
* // account field is skipped so it is not rendered as the zero address.
* for (auto const& field : stx)
* {
* if (field.getSType() != STI_ACCOUNT || field.isDefault())
* continue;
* if (auto const key = telemetry::accountFieldAttributeKey(field.getFName()))
* span.setAttribute(*key, toBase58(stx.getAccountID(field.getFName())));
* }
* @endcode
*
* @code
* // Edge case: a field that is not a top-level transaction account has
* // no key, so a caller must test the optional before using it.
* accountFieldAttributeKey(sfFee); // == std::nullopt
* accountFieldAttributeKey(sfLowSponsor); // == std::nullopt
* @endcode
*
* @note Only top-level fields are covered. Accounts nested in Signers, in a
* Batch's inner transactions, or as the issuer inside an Amount are not
* emitted.
* @note accountFieldAttributeKey() is thread-safe. Its table is built once
* on first use and is read-only afterwards.
*/
#include <xrpl/telemetry/SpanNames.h>
#include <optional>
#include <string_view>
namespace xrpl {
class SField;
} // namespace xrpl
namespace xrpl::telemetry {
namespace tx_account_span::attr {
/**
* "tx_account" — the sending account (`Account`). Every transaction has one.
*/
inline constexpr auto account = makeStr("tx_account");
/**
* "tx_destination" — the receiving account (`Destination`).
*/
inline constexpr auto destination = makeStr("tx_destination");
/**
* "tx_owner" — the owner of the object acted on (`Owner`).
*/
inline constexpr auto owner = makeStr("tx_owner");
/**
* "tx_issuer" — the issuer named by the transaction (`Issuer`).
*/
inline constexpr auto issuer = makeStr("tx_issuer");
/**
* "tx_authorize" — the account being authorised (`Authorize`).
*/
inline constexpr auto authorize = makeStr("tx_authorize");
/**
* "tx_unauthorize" — the account whose authorisation is removed (`Unauthorize`).
*/
inline constexpr auto unauthorize = makeStr("tx_unauthorize");
/**
* "tx_regular_key" — the regular key being set (`RegularKey`).
*/
inline constexpr auto regularKey = makeStr("tx_regular_key");
/**
* "tx_nftoken_minter" — the authorised NFToken minter (`NFTokenMinter`).
*/
inline constexpr auto nftokenMinter = makeStr("tx_nftoken_minter");
/**
* "tx_holder" — the token holder acted on (`Holder`).
*/
inline constexpr auto holder = makeStr("tx_holder");
/**
* "tx_delegate" — the delegate signing on the sender's behalf (`Delegate`).
*/
inline constexpr auto delegate = makeStr("tx_delegate");
/**
* "tx_sponsor" — the account paying the fee or reserve (`Sponsor`).
*/
inline constexpr auto sponsor = makeStr("tx_sponsor");
/**
* "tx_sponsee" — the account being sponsored (`Sponsee`).
*/
inline constexpr auto sponsee = makeStr("tx_sponsee");
/**
* "tx_counterparty" — the other party to a loan (`Counterparty`).
*/
inline constexpr auto counterparty = makeStr("tx_counterparty");
/**
* "tx_counterparty_sponsor" — the counterparty's sponsor (`CounterpartySponsor`).
*/
inline constexpr auto counterpartySponsor = makeStr("tx_counterparty_sponsor");
/**
* "tx_subject" — the subject of a credential (`Subject`).
*/
inline constexpr auto subject = makeStr("tx_subject");
/**
* "tx_other_chain_source" — the source account on the other chain (`OtherChainSource`).
*/
inline constexpr auto otherChainSource = makeStr("tx_other_chain_source");
/**
* "tx_other_chain_destination" — destination on the other chain (`OtherChainDestination`).
*/
inline constexpr auto otherChainDestination = makeStr("tx_other_chain_destination");
/**
* "tx_attestation_signer_account" — the attestation signer (`AttestationSignerAccount`).
*/
inline constexpr auto attestationSignerAccount = makeStr("tx_attestation_signer_account");
/**
* "tx_attestation_reward_account" — attestation reward account (`AttestationRewardAccount`).
*/
inline constexpr auto attestationRewardAccount = makeStr("tx_attestation_reward_account");
} // namespace tx_account_span::attr
/**
* Look up the span attribute key for an account-typed transaction field.
*
* @param field The protocol field, as returned by STBase::getFName().
* @return The `tx_*` key for a top-level transaction account field, or
* nullopt when the field is not account-typed or is carried only by ledger
* entries and inner objects.
*/
[[nodiscard]] std::optional<std::string_view>
accountFieldAttributeKey(SField const& field);
} // namespace xrpl::telemetry