mirror of
https://github.com/XRPLF/rippled.git
synced 2026-09-27 07:26:51 +00:00
Merge branch 'pratik/otel-phase7-native-metrics' into pratik/otel-phase8-log-correlation
This commit is contained in:
@@ -196,6 +196,7 @@ tests.libxrpl > xrpl.nodestore
|
||||
tests.libxrpl > xrpl.peerfinder
|
||||
tests.libxrpl > xrpl.protocol
|
||||
tests.libxrpl > xrpl.protocol_autogen
|
||||
tests.libxrpl > xrpl.rdb
|
||||
tests.libxrpl > xrpl.resource
|
||||
tests.libxrpl > xrpl.server
|
||||
tests.libxrpl > xrpl.shamap
|
||||
|
||||
@@ -62,13 +62,17 @@ The parser `makeTelemetrySetup()` in `src/libxrpl/telemetry/TelemetryConfig.cpp`
|
||||
|
||||
### 5.3.1 ApplicationImp Changes
|
||||
|
||||
> **Deferred identity**: The node public key (`nodeIdentity_`) is not
|
||||
> available during `ApplicationImp`'s member initializer list — it is
|
||||
> resolved later in `setup()`. The `Telemetry` object is therefore
|
||||
> constructed with an empty `serviceInstanceId` and patched via
|
||||
> `setServiceInstanceId()` once `setup()` has called `getNodeIdentity()`.
|
||||
> **Identity before construction**: telemetry stamps the node public key into
|
||||
> resources that are immutable once built, and it builds them during
|
||||
> `ApplicationImp`'s member initializer list. So `Main.cpp` calls
|
||||
> `resolveNodeIdentity()` first, from the config and command line alone, and
|
||||
> passes the keypair to `makeApplication()`. It never comes back empty: a
|
||||
> configured `[node_seed]` decides it, else the wallet database supplies it if
|
||||
> one already exists, else it is minted. `ApplicationImp::setup()` then calls
|
||||
> `getNodeIdentity()`, which stores that keypair when the wallet holds none and
|
||||
> otherwise adopts what the wallet holds.
|
||||
|
||||
`ApplicationImp` (in `src/xrpld/app/main/Application.cpp`) owns a `std::unique_ptr<telemetry::Telemetry> telemetry_`. It is built in the member initializer list via `makeTelemetry(makeTelemetrySetup(...))` with an empty `serviceInstanceId`, then patched in `setup()` by calling `setServiceInstanceId()` with the Base58 node public key (unless the user supplied a custom `service_instance_id`). `start()` and `run()` forward to `telemetry_->start()` / `telemetry_->stop()`, and `getTelemetry()` returns the owned instance.
|
||||
`ApplicationImp` (in `src/xrpld/app/main/Application.cpp`) owns a `std::pair<PublicKey, SecretKey> nodeIdentity_`, declared before `std::unique_ptr<telemetry::Telemetry> telemetry_` so the resource can be built from it. `telemetry_` is built in the member initializer list via `makeTelemetry(makeTelemetrySetup(...))` with that key as `serviceInstanceId` (unless the user supplied a custom `service_instance_id`). `setup()` still calls `setServiceInstanceId()`, which now matters only where the stored key differs from the resolved one, and reaches the tracer resource alone. `start()` and `run()` forward to `telemetry_->start()` / `telemetry_->stop()`, and `getTelemetry()` returns the owned instance.
|
||||
|
||||
### 5.3.2 ServiceRegistry Interface Addition
|
||||
|
||||
|
||||
@@ -275,8 +275,10 @@ trace_peer=1
|
||||
trace_ledger=1
|
||||
|
||||
[insight]
|
||||
# server=otel is the only load-bearing key here -- it selects OTelCollector.
|
||||
# The export endpoint comes from [telemetry] metrics_endpoint, and [insight]'s
|
||||
# own service_instance_id/service_name keys are ignored.
|
||||
server=otel
|
||||
endpoint=http://localhost:4318/v1/metrics
|
||||
|
||||
[rpc_startup]
|
||||
{ "command": "log_level", "severity": "info" }
|
||||
|
||||
@@ -102,6 +102,22 @@ clearNodeIdentity(soci::session& session);
|
||||
std::optional<std::pair<PublicKey, SecretKey>>
|
||||
readNodeIdentity(soci::session& session);
|
||||
|
||||
/**
|
||||
* Persist a keypair as this node's identity.
|
||||
*
|
||||
* Write-only counterpart of readNodeIdentity(). The caller must have found the
|
||||
* table empty: this inserts a row without clearing, so storing twice leaves two
|
||||
* and readNodeIdentity() then returns whichever the query yields first.
|
||||
*
|
||||
* Exists because xrpld resolves its identity before the Application, and so
|
||||
* before any database, is built; setup() persists that keypair here.
|
||||
*
|
||||
* @param session Session with the database.
|
||||
* @param keys The keypair to store.
|
||||
*/
|
||||
void
|
||||
storeNodeIdentity(soci::session& session, std::pair<PublicKey, SecretKey> const& keys);
|
||||
|
||||
/**
|
||||
* Returns a stable public and private key for this node.
|
||||
*
|
||||
|
||||
@@ -133,6 +133,25 @@ inline constexpr std::string_view kMeterName{"xrpld"};
|
||||
* OTel instrumentation scope version reported for the meter.
|
||||
*/
|
||||
inline constexpr std::string_view kMeterVersion{"1.0.0"};
|
||||
|
||||
/**
|
||||
* A meter whose instruments record nothing.
|
||||
*
|
||||
* For every path that must hand out a usable meter without a pipeline behind
|
||||
* it: telemetry disabled, or an exporter that failed to build. Callers then
|
||||
* need no null check, because an instrument always comes back.
|
||||
*
|
||||
* Two details are easy to get wrong alone, which is why this is shared: the
|
||||
* provider must be reached through a base `MeterProvider` pointer, because
|
||||
* `NoopMeterProvider`'s override hides the base class's defaulted overload;
|
||||
* and the version must be @ref kMeterVersion, or the meter identity differs
|
||||
* from the one the histogram views select on.
|
||||
*
|
||||
* @param name Instrumentation scope name to report.
|
||||
* @return An inert meter. Never empty.
|
||||
*/
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<opentelemetry::metrics::Meter>
|
||||
noopMeter(std::string_view name = kMeterName);
|
||||
#endif
|
||||
|
||||
/**
|
||||
|
||||
@@ -511,17 +511,25 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Register a hook for periodic invocation.
|
||||
* @param hook Pointer to the hook to register.
|
||||
*
|
||||
* Takes the owning shared_ptr so the list can store a weak reference.
|
||||
* Called from makeHook() rather than the hook's constructor, because a
|
||||
* weak_ptr cannot be formed until the shared_ptr owns the object.
|
||||
*
|
||||
* @param hook Owning pointer to the hook to register.
|
||||
*/
|
||||
void
|
||||
addHook(OTelHookImpl* hook);
|
||||
addHook(std::shared_ptr<OTelHookImpl> const& hook);
|
||||
|
||||
/**
|
||||
* @brief Unregister a hook.
|
||||
* @param hook Pointer to the hook to unregister.
|
||||
* @brief Drop entries for hooks that have been destroyed.
|
||||
*
|
||||
* Called from ~OTelHookImpl. The dying hook's weak_ptr has already
|
||||
* expired by then, so the entry is identified by expiry rather than by
|
||||
* address.
|
||||
*/
|
||||
void
|
||||
removeHook(OTelHookImpl* hook);
|
||||
removeExpiredHooks();
|
||||
|
||||
/**
|
||||
* @brief Invoke all registered hooks.
|
||||
@@ -597,8 +605,16 @@ private:
|
||||
|
||||
/**
|
||||
* Registered hooks called during observable callbacks.
|
||||
*
|
||||
* Weak, not owning, and not raw. callHooks() must invoke handlers with
|
||||
* mutex_ released, because a handler may drop the last reference to a
|
||||
* hook and ~OTelHookImpl re-acquires mutex_. A raw pointer copied out of
|
||||
* this list could therefore be dangling by the time it is dereferenced.
|
||||
* Locking a weak_ptr instead keeps the hook alive for exactly the
|
||||
* duration of its own handler call, and an already-destroyed hook is
|
||||
* skipped rather than followed.
|
||||
*/
|
||||
std::vector<OTelHookImpl*> hooks_;
|
||||
std::vector<std::weak_ptr<OTelHookImpl>> hooks_;
|
||||
|
||||
/**
|
||||
* Registered gauges read during observable callbacks.
|
||||
@@ -634,12 +650,14 @@ private:
|
||||
OTelHookImpl::OTelHookImpl(HandlerType handler, std::shared_ptr<OTelCollectorImp> impl)
|
||||
: impl_(std::move(impl)), handler_(std::move(handler))
|
||||
{
|
||||
impl_->addHook(this);
|
||||
// Registration happens in OTelCollectorImp::makeHook(), not here: the
|
||||
// list holds weak references, and no weak_ptr to this object exists
|
||||
// until the owning shared_ptr does.
|
||||
}
|
||||
|
||||
OTelHookImpl::~OTelHookImpl()
|
||||
{
|
||||
impl_->removeHook(this);
|
||||
impl_->removeExpiredHooks();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -849,7 +867,9 @@ OTelCollectorImp::~OTelCollectorImp()
|
||||
Hook
|
||||
OTelCollectorImp::makeHook(HookImpl::HandlerType const& handler)
|
||||
{
|
||||
return Hook(std::make_shared<OTelHookImpl>(handler, shared_from_this()));
|
||||
auto hook = std::make_shared<OTelHookImpl>(handler, shared_from_this());
|
||||
addHook(hook);
|
||||
return Hook(hook);
|
||||
}
|
||||
|
||||
Counter
|
||||
@@ -883,17 +903,17 @@ OTelCollectorImp::makeMeter(std::string const& name)
|
||||
}
|
||||
|
||||
void
|
||||
OTelCollectorImp::addHook(OTelHookImpl* hook)
|
||||
OTelCollectorImp::addHook(std::shared_ptr<OTelHookImpl> const& hook)
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
hooks_.push_back(hook);
|
||||
hooks_.emplace_back(hook);
|
||||
}
|
||||
|
||||
void
|
||||
OTelCollectorImp::removeHook(OTelHookImpl* hook)
|
||||
OTelCollectorImp::removeExpiredHooks()
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
std::erase(hooks_, hook);
|
||||
std::erase_if(hooks_, [](std::weak_ptr<OTelHookImpl> const& hook) { return hook.expired(); });
|
||||
}
|
||||
|
||||
void
|
||||
@@ -910,15 +930,22 @@ OTelCollectorImp::callHooks()
|
||||
|
||||
// Copy the hook list under the lock, then invoke handlers outside it.
|
||||
// A handler may drop the last reference to an OTelHookImpl, whose
|
||||
// destructor calls removeHook() and re-acquires mutex_; invoking
|
||||
// handlers while holding the (non-recursive) lock would deadlock.
|
||||
std::vector<OTelHookImpl*> hooks;
|
||||
// destructor re-acquires mutex_; invoking handlers while holding the
|
||||
// (non-recursive) lock would deadlock.
|
||||
std::vector<std::weak_ptr<OTelHookImpl>> hooks;
|
||||
{
|
||||
std::scoped_lock const lock(mutex_);
|
||||
hooks = hooks_;
|
||||
}
|
||||
for (auto* hook : hooks)
|
||||
hook->callHandler();
|
||||
|
||||
// Locking each entry keeps that hook alive across its own handler call,
|
||||
// so releasing mutex_ above cannot leave a dangling reference. A hook
|
||||
// destroyed since the snapshot was taken locks to null and is skipped.
|
||||
for (auto const& weakHook : hooks)
|
||||
{
|
||||
if (auto const hook = weakHook.lock())
|
||||
hook->callHandler();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -171,6 +171,16 @@ readNodeIdentity(soci::session& session)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
storeNodeIdentity(soci::session& session, std::pair<PublicKey, SecretKey> const& keys)
|
||||
{
|
||||
session << std::format(
|
||||
"INSERT INTO NodeIdentity (PublicKey,PrivateKey) "
|
||||
"VALUES ('{}','{}');",
|
||||
toBase58(TokenType::NodePublic, keys.first),
|
||||
toBase58(TokenType::NodePrivate, keys.second));
|
||||
}
|
||||
|
||||
std::pair<PublicKey, SecretKey>
|
||||
getNodeIdentity(soci::session& session)
|
||||
{
|
||||
@@ -178,15 +188,9 @@ getNodeIdentity(soci::session& session)
|
||||
return *stored;
|
||||
|
||||
// If a valid identity wasn't found, we randomly generate a new one:
|
||||
auto [newpublicKey, newsecretKey] = randomKeyPair(KeyType::Secp256k1);
|
||||
|
||||
session << std::format(
|
||||
"INSERT INTO NodeIdentity (PublicKey,PrivateKey) "
|
||||
"VALUES ('{}','{}');",
|
||||
toBase58(TokenType::NodePublic, newpublicKey),
|
||||
toBase58(TokenType::NodePrivate, newsecretKey));
|
||||
|
||||
return {newpublicKey, newsecretKey};
|
||||
auto const keys = randomKeyPair(KeyType::Secp256k1);
|
||||
storeNodeIdentity(session, keys);
|
||||
return keys;
|
||||
}
|
||||
|
||||
std::unordered_set<PeerReservation, beast::Uhash<>, KeyEqual>
|
||||
|
||||
@@ -261,11 +261,8 @@ public:
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<metrics_api::Meter>
|
||||
getMeter(std::string_view name) override
|
||||
{
|
||||
// Serve a meter from a process-wide noop provider, mirroring the
|
||||
// noop tracer above. Instruments created from it are inert.
|
||||
static auto noopProvider = opentelemetry::nostd::shared_ptr<metrics_api::MeterProvider>(
|
||||
new metrics_api::NoopMeterProvider());
|
||||
return noopProvider->GetMeter(std::string(name), std::string(kMeterVersion));
|
||||
// Mirrors the noop tracer above: instruments created from it are inert.
|
||||
return noopMeter(name);
|
||||
}
|
||||
|
||||
[[nodiscard]] opentelemetry::nostd::shared_ptr<trace_api::Span>
|
||||
@@ -703,6 +700,16 @@ public:
|
||||
|
||||
} // namespace
|
||||
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::Meter>
|
||||
noopMeter(std::string_view name)
|
||||
{
|
||||
// One provider for the process: it holds a single inert meter, so nothing
|
||||
// is gained by building another.
|
||||
static auto const provider = opentelemetry::nostd::shared_ptr<metrics_api::MeterProvider>(
|
||||
new metrics_api::NoopMeterProvider());
|
||||
return provider->GetMeter(std::string(name), std::string(kMeterVersion));
|
||||
}
|
||||
|
||||
opentelemetry::exporter::otlp::OtlpHttpExporterOptions
|
||||
makeTraceExporterOptions(Telemetry::Setup const& setup)
|
||||
{
|
||||
|
||||
208
src/tests/libxrpl/beast/insight/OTelCollectorHooks.cpp
Normal file
208
src/tests/libxrpl/beast/insight/OTelCollectorHooks.cpp
Normal file
@@ -0,0 +1,208 @@
|
||||
#ifdef XRPL_ENABLE_TELEMETRY
|
||||
|
||||
#include <xrpl/beast/insight/Collector.h>
|
||||
#include <xrpl/beast/insight/Gauge.h>
|
||||
#include <xrpl/beast/insight/Hook.h>
|
||||
#include <xrpl/beast/insight/OTelCollector.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <opentelemetry/metrics/meter_provider.h>
|
||||
#include <opentelemetry/metrics/provider.h>
|
||||
#include <opentelemetry/nostd/function_ref.h>
|
||||
#include <opentelemetry/nostd/shared_ptr.h>
|
||||
#include <opentelemetry/sdk/metrics/instruments.h>
|
||||
#include <opentelemetry/sdk/metrics/meter_provider.h>
|
||||
#include <opentelemetry/sdk/metrics/meter_provider_factory.h>
|
||||
#include <opentelemetry/sdk/metrics/metric_reader.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace beast::insight {
|
||||
|
||||
namespace metrics_api = opentelemetry::metrics;
|
||||
namespace metrics_sdk = opentelemetry::sdk::metrics;
|
||||
|
||||
/**
|
||||
* A MetricReader that collects only when the test asks it to.
|
||||
*
|
||||
* The SDK ships only PeriodicExportingMetricReader, whose background thread
|
||||
* would make these tests depend on timing. MetricReader::Collect() is public
|
||||
* and synchronous, so a minimal subclass lets a test drive one collection pass
|
||||
* on the calling thread. That pass is what invokes an observable gauge's
|
||||
* callback, which is the only path that reaches the collector's hooks.
|
||||
*
|
||||
* @code
|
||||
* auto reader = std::make_shared<ManualMetricReader>();
|
||||
* provider->AddMetricReader(reader);
|
||||
* reader->collectOnce(); // runs every registered observable callback
|
||||
* @endcode
|
||||
*/
|
||||
class ManualMetricReader : public metrics_sdk::MetricReader
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Run exactly one collection pass, discarding the metric data.
|
||||
*
|
||||
* The tests assert on hook side effects, not on exported points, so the
|
||||
* callback returns true without inspecting what it was handed.
|
||||
*/
|
||||
void
|
||||
collectOnce()
|
||||
{
|
||||
Collect([](metrics_sdk::ResourceMetrics&) { return true; });
|
||||
}
|
||||
|
||||
metrics_sdk::AggregationTemporality
|
||||
GetAggregationTemporality(metrics_sdk::InstrumentType) const noexcept override
|
||||
{
|
||||
return metrics_sdk::AggregationTemporality::kCumulative;
|
||||
}
|
||||
|
||||
bool
|
||||
OnForceFlush(std::chrono::microseconds) noexcept override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
OnShutDown(std::chrono::microseconds) noexcept override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Installs a real SDK MeterProvider so observable gauges actually fire.
|
||||
*
|
||||
* OTelCollector takes its Meter from the global provider. Under the default
|
||||
* noop provider an observable gauge's callback is never invoked, so a hook
|
||||
* test would pass whatever the collector did. The fixture swaps in an SDK
|
||||
* provider with a ManualMetricReader and restores the previous global provider
|
||||
* afterwards, so it leaks no state into other telemetry tests in this binary.
|
||||
*/
|
||||
class OTelCollectorHooks : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
previous_ = metrics_api::Provider::GetMeterProvider();
|
||||
reader_ = std::make_shared<ManualMetricReader>();
|
||||
auto provider = metrics_sdk::MeterProviderFactory::Create();
|
||||
provider->AddMetricReader(reader_);
|
||||
provider_ = std::shared_ptr<metrics_sdk::MeterProvider>(std::move(provider));
|
||||
metrics_api::Provider::SetMeterProvider(
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::MeterProvider>(provider_));
|
||||
}
|
||||
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
metrics_api::Provider::SetMeterProvider(previous_);
|
||||
provider_.reset();
|
||||
reader_.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Build a collector, plus the armed gauge that drives its hooks.
|
||||
*
|
||||
* A collection pass only reaches the hooks through an observable gauge's
|
||||
* callback, and a gauge is armed by onCollectionReady(), so every test
|
||||
* needs both. The gauge is returned because dropping it would unregister
|
||||
* the callback.
|
||||
*
|
||||
* Each test builds its own collector: the hook debounce is keyed to the
|
||||
* time of the last invocation, which starts unset, so the first collection
|
||||
* on a fresh collector always runs the hooks.
|
||||
*/
|
||||
static std::pair<Collector::ptr, Gauge>
|
||||
makeArmedCollector()
|
||||
{
|
||||
auto collector = OTelCollector::New(
|
||||
"http://127.0.0.1:4318/v1/metrics",
|
||||
"",
|
||||
"test-instance",
|
||||
"xrpld",
|
||||
"test",
|
||||
Journal(Journal::getNullSink()));
|
||||
auto gauge = collector->makeGauge("hook_test_gauge");
|
||||
collector->onCollectionReady();
|
||||
return {std::move(collector), std::move(gauge)};
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<metrics_api::MeterProvider> previous_;
|
||||
std::shared_ptr<ManualMetricReader> reader_;
|
||||
std::shared_ptr<metrics_sdk::MeterProvider> provider_;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. A hook that is still alive runs on a collection pass.
|
||||
// This is the registration path: makeHook() puts the hook on the
|
||||
// collector's list, and an observable gauge callback invokes it. Without
|
||||
// this, a hook that is never registered is indistinguishable from one that
|
||||
// is registered and skipped.
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_F(OTelCollectorHooks, live_hook_runs_once_per_collection)
|
||||
{
|
||||
auto [collector, gauge] = makeArmedCollector();
|
||||
|
||||
std::size_t calls = 0;
|
||||
auto const hook = collector->makeHook([&calls] { ++calls; });
|
||||
|
||||
reader_->collectOnce();
|
||||
|
||||
EXPECT_EQ(calls, 1u);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. A hook destroyed before the collection pass is skipped, not called.
|
||||
// The collector holds weak references, so the destroyed hook's entry locks
|
||||
// to null. Asserting zero (not "did not crash") is what makes this a real
|
||||
// check: a stale entry that was still followed would run the handler and
|
||||
// increment the counter through freed memory.
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_F(OTelCollectorHooks, destroyed_hook_is_skipped)
|
||||
{
|
||||
auto [collector, gauge] = makeArmedCollector();
|
||||
|
||||
std::size_t calls = 0;
|
||||
{
|
||||
auto const hook = collector->makeHook([&calls] { ++calls; });
|
||||
}
|
||||
|
||||
reader_->collectOnce();
|
||||
|
||||
EXPECT_EQ(calls, 0u);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. Destroying one hook leaves its siblings registered.
|
||||
// Guards the pruning step: removeExpiredHooks() erases by expiry rather
|
||||
// than by address, so an over-broad predicate would drop live hooks too and
|
||||
// silently stop their metrics updating.
|
||||
// ---------------------------------------------------------------------------
|
||||
TEST_F(OTelCollectorHooks, destroying_one_hook_keeps_the_others)
|
||||
{
|
||||
auto [collector, gauge] = makeArmedCollector();
|
||||
|
||||
std::size_t kept = 0;
|
||||
std::size_t dropped = 0;
|
||||
auto const keptHook = collector->makeHook([&kept] { ++kept; });
|
||||
{
|
||||
auto const droppedHook = collector->makeHook([&dropped] { ++dropped; });
|
||||
}
|
||||
|
||||
reader_->collectOnce();
|
||||
|
||||
EXPECT_EQ(kept, 1u);
|
||||
EXPECT_EQ(dropped, 0u);
|
||||
}
|
||||
|
||||
} // namespace beast::insight
|
||||
|
||||
#endif // XRPL_ENABLE_TELEMETRY
|
||||
158
src/tests/libxrpl/server/NodeIdentity.cpp
Normal file
158
src/tests/libxrpl/server/NodeIdentity.cpp
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* @file NodeIdentity.cpp
|
||||
* GTest unit tests for the wallet database's node-identity storage.
|
||||
*
|
||||
* Three functions share one table, `NodeIdentity`, and the split between them
|
||||
* is what the telemetry startup order depends on: `readNodeIdentity()` only
|
||||
* reads, `storeNodeIdentity()` only writes, and `getNodeIdentity()` reads then
|
||||
* writes a fresh key when the table is empty. `xrpld` resolves its identity
|
||||
* before the Application exists and persists it later, so the store step has
|
||||
* to be callable on its own and has to be idempotent-by-read: a second run
|
||||
* must return the first run's key, not a new one.
|
||||
*
|
||||
* Each test gets its own database file in a temporary directory, so nothing
|
||||
* here depends on order or on the developer's data directory.
|
||||
*/
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/rdb/DatabaseCon.h>
|
||||
#include <xrpl/server/Wallet.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* A wallet database in its own temporary directory, removed on destruction.
|
||||
*
|
||||
* `makeTestWalletDB()` creates the schema, so every fixture starts with an
|
||||
* empty `NodeIdentity` table.
|
||||
*/
|
||||
class TempWalletDb
|
||||
{
|
||||
public:
|
||||
explicit TempWalletDb(std::string const& name)
|
||||
: dir_(std::filesystem::temp_directory_path() / ("xrpl-node-identity-" + name))
|
||||
{
|
||||
std::filesystem::remove_all(dir_);
|
||||
std::filesystem::create_directories(dir_);
|
||||
|
||||
DatabaseCon::Setup setup;
|
||||
setup.dataDir = dir_;
|
||||
db_ = makeTestWalletDB(setup, "wallet.db", beast::Journal{beast::Journal::getNullSink()});
|
||||
}
|
||||
|
||||
~TempWalletDb()
|
||||
{
|
||||
db_.reset();
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(dir_, ec);
|
||||
}
|
||||
|
||||
TempWalletDb(TempWalletDb const&) = delete;
|
||||
TempWalletDb&
|
||||
operator=(TempWalletDb const&) = delete;
|
||||
|
||||
[[nodiscard]] DatabaseCon&
|
||||
operator*() const noexcept
|
||||
{
|
||||
return *db_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path dir_;
|
||||
std::unique_ptr<DatabaseCon> db_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(WalletNodeIdentity, store_then_read_returns_the_same_pair)
|
||||
{
|
||||
// The store step exists so a key minted before the Application is built
|
||||
// can be persisted afterwards. Reading it back must give the same pair, or
|
||||
// the two halves of one run report two identities.
|
||||
TempWalletDb wallet("store-then-read");
|
||||
auto const minted = randomKeyPair(KeyType::Secp256k1);
|
||||
|
||||
{
|
||||
auto db = (*wallet).checkoutDb();
|
||||
ASSERT_FALSE(readNodeIdentity(*db).has_value()) << "a fresh wallet must hold no identity";
|
||||
storeNodeIdentity(*db, minted);
|
||||
}
|
||||
|
||||
auto db = (*wallet).checkoutDb();
|
||||
auto const stored = readNodeIdentity(*db);
|
||||
ASSERT_TRUE(stored.has_value());
|
||||
EXPECT_EQ(stored->first, minted.first);
|
||||
EXPECT_EQ(stored->second, minted.second);
|
||||
}
|
||||
|
||||
TEST(WalletNodeIdentity, store_does_not_replace_an_existing_identity)
|
||||
{
|
||||
// getNodeIdentity() is the read-or-mint path and must keep the first key,
|
||||
// so a restart does not change the node's identity on the network. The
|
||||
// stored pair wins over anything a later caller offers.
|
||||
TempWalletDb wallet("no-replace");
|
||||
auto db = (*wallet).checkoutDb();
|
||||
|
||||
auto const first = getNodeIdentity(*db);
|
||||
auto const other = randomKeyPair(KeyType::Secp256k1);
|
||||
ASSERT_NE(first.first, other.first)
|
||||
<< "the two pairs must differ for this test to mean anything";
|
||||
|
||||
storeNodeIdentity(*db, other);
|
||||
|
||||
auto const stored = readNodeIdentity(*db);
|
||||
ASSERT_TRUE(stored.has_value());
|
||||
EXPECT_EQ(stored->first, first.first);
|
||||
EXPECT_EQ(getNodeIdentity(*db).first, first.first);
|
||||
}
|
||||
|
||||
TEST(WalletNodeIdentity, clear_then_store_installs_the_new_pair)
|
||||
{
|
||||
// --newnodeid clears the row and then persists the freshly minted pair.
|
||||
// Both steps are needed: clearing alone would leave the node with no
|
||||
// stored identity at all.
|
||||
TempWalletDb wallet("clear-then-store");
|
||||
auto db = (*wallet).checkoutDb();
|
||||
|
||||
auto const first = getNodeIdentity(*db);
|
||||
auto const replacement = randomKeyPair(KeyType::Secp256k1);
|
||||
ASSERT_NE(first.first, replacement.first);
|
||||
|
||||
clearNodeIdentity(*db);
|
||||
EXPECT_FALSE(readNodeIdentity(*db).has_value()) << "clear must leave the table empty";
|
||||
|
||||
storeNodeIdentity(*db, replacement);
|
||||
auto const stored = readNodeIdentity(*db);
|
||||
ASSERT_TRUE(stored.has_value());
|
||||
EXPECT_EQ(stored->first, replacement.first);
|
||||
EXPECT_EQ(stored->second, replacement.second);
|
||||
}
|
||||
|
||||
TEST(WalletNodeIdentity, get_mints_and_persists_when_the_table_is_empty)
|
||||
{
|
||||
// The mint path must persist, not just return: a second call has to give
|
||||
// the same key. This is the property --newnodeid relies on to be
|
||||
// meaningful, and the one a caller that only reads would break.
|
||||
TempWalletDb wallet("mint-and-persist");
|
||||
auto db = (*wallet).checkoutDb();
|
||||
|
||||
auto const minted = getNodeIdentity(*db);
|
||||
|
||||
auto const stored = readNodeIdentity(*db);
|
||||
ASSERT_TRUE(stored.has_value()) << "getNodeIdentity() must persist what it mints";
|
||||
EXPECT_EQ(stored->first, minted.first);
|
||||
EXPECT_EQ(getNodeIdentity(*db).first, minted.first);
|
||||
}
|
||||
@@ -193,10 +193,7 @@ public:
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::metrics::Meter>
|
||||
getMeter(std::string_view name) override
|
||||
{
|
||||
static auto noopProvider =
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::metrics::MeterProvider>(
|
||||
new opentelemetry::metrics::NoopMeterProvider());
|
||||
return noopProvider->GetMeter(std::string(name), std::string(kMeterVersion));
|
||||
return noopMeter(name);
|
||||
}
|
||||
|
||||
opentelemetry::nostd::shared_ptr<opentelemetry::trace::Span>
|
||||
|
||||
@@ -80,9 +80,11 @@
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/KeyType.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/SystemParameters.h> // IWYU pragma: keep
|
||||
#include <xrpl/protocol/jss.h>
|
||||
@@ -220,6 +222,13 @@ public:
|
||||
|
||||
beast::Journal journal_;
|
||||
std::unique_ptr<perf::PerfLog> perfLog_;
|
||||
/**
|
||||
* This node's keypair, resolved before construction by
|
||||
* resolveNodeIdentity() and persisted by setup(). Declared before
|
||||
* telemetry_ because that builds resource attributes from it, and they are
|
||||
* immutable once built.
|
||||
*/
|
||||
std::pair<PublicKey, SecretKey> nodeIdentity_;
|
||||
std::unique_ptr<telemetry::Telemetry> telemetry_;
|
||||
Application::MutexType masterMutex_;
|
||||
|
||||
@@ -236,7 +245,6 @@ public:
|
||||
NodeCache tempNodeCache_;
|
||||
CachedSLEs cachedSLEs_;
|
||||
std::unique_ptr<NetworkIDService> networkIDService_;
|
||||
std::optional<std::pair<PublicKey, SecretKey>> nodeIdentity_;
|
||||
ValidatorKeys const validatorKeys_;
|
||||
|
||||
std::unique_ptr<resource::Manager> resourceManager_;
|
||||
@@ -317,7 +325,7 @@ public:
|
||||
std::unique_ptr<Config> config,
|
||||
std::unique_ptr<Logs> logs,
|
||||
std::unique_ptr<TimeKeeper> timeKeeper,
|
||||
std::optional<std::string> const& nodePublicKey)
|
||||
std::pair<PublicKey, SecretKey> const& resolvedIdentity)
|
||||
: BasicApp(numberOfThreads(*config))
|
||||
, config_(std::move(config))
|
||||
, logs_(std::move(logs))
|
||||
@@ -331,15 +339,16 @@ public:
|
||||
*this,
|
||||
logs_->journal("PerfLog"),
|
||||
[this] { signalStop("PerfLog"); }))
|
||||
, nodeIdentity_(resolvedIdentity)
|
||||
// Telemetry publishes the MeterProvider on construction, so it must
|
||||
// precede collectorManager_ below and every subsystem that creates an
|
||||
// instrument. Its resource is immutable, so the instance id has to be
|
||||
// supplied now; empty means this run reports none.
|
||||
// supplied now, from the identity resolved above.
|
||||
, telemetry_(
|
||||
telemetry::makeTelemetry(
|
||||
telemetry::makeTelemetrySetup(
|
||||
config_->section("telemetry"),
|
||||
nodePublicKey.value_or(""),
|
||||
toBase58(TokenType::NodePublic, nodeIdentity_.first),
|
||||
build_info::getVersionString(),
|
||||
config_->networkId),
|
||||
logs_->journal("Telemetry")))
|
||||
@@ -619,10 +628,7 @@ public:
|
||||
std::pair<PublicKey, SecretKey> const&
|
||||
nodeIdentity() override
|
||||
{
|
||||
if (nodeIdentity_)
|
||||
return *nodeIdentity_;
|
||||
|
||||
logicError("Accessing Application::nodeIdentity() before it is initialized.");
|
||||
return nodeIdentity_;
|
||||
}
|
||||
|
||||
std::optional<PublicKey const>
|
||||
@@ -1306,12 +1312,15 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
return false;
|
||||
}
|
||||
|
||||
nodeIdentity_ = getNodeIdentity(*this, cmdline);
|
||||
// Persist the identity resolved before construction, or adopt the one the
|
||||
// wallet already holds. Telemetry is already reporting the resolved key.
|
||||
nodeIdentity_ = getNodeIdentity(*this, cmdline, nodeIdentity_);
|
||||
|
||||
// The metrics resource was fixed at construction, but the tracer resource is
|
||||
// built by start() below, so a key minted just now can still reach spans.
|
||||
// built by start() below, so the stored key still reaches spans if it
|
||||
// differs from the resolved one.
|
||||
if (!config_->section("telemetry").exists("service_instance_id"))
|
||||
telemetry_->setServiceInstanceId(toBase58(TokenType::NodePublic, nodeIdentity_->first));
|
||||
telemetry_->setServiceInstanceId(toBase58(TokenType::NodePublic, nodeIdentity_.first));
|
||||
|
||||
// Start telemetry here, not in start(). Spans are emitted during the rest
|
||||
// of setup() — the first consensus round in beginConsensus() below — and
|
||||
@@ -2298,7 +2307,13 @@ makeApplication(
|
||||
std::unique_ptr<Logs> logs,
|
||||
std::unique_ptr<TimeKeeper> timeKeeper)
|
||||
{
|
||||
return makeApplication(std::move(config), std::move(logs), std::move(timeKeeper), std::nullopt);
|
||||
// No identity supplied, so mint one. setup() stores it if the wallet holds
|
||||
// none, which is what a standalone run and a test Application do anyway.
|
||||
return makeApplication(
|
||||
std::move(config),
|
||||
std::move(logs),
|
||||
std::move(timeKeeper),
|
||||
randomKeyPair(KeyType::Secp256k1));
|
||||
}
|
||||
|
||||
std::unique_ptr<Application>
|
||||
@@ -2306,10 +2321,10 @@ makeApplication(
|
||||
std::unique_ptr<Config> config,
|
||||
std::unique_ptr<Logs> logs,
|
||||
std::unique_ptr<TimeKeeper> timeKeeper,
|
||||
std::optional<std::string> const& nodePublicKey)
|
||||
std::pair<PublicKey, SecretKey> const& nodeIdentity)
|
||||
{
|
||||
return std::make_unique<ApplicationImp>(
|
||||
std::move(config), std::move(logs), std::move(timeKeeper), nodePublicKey);
|
||||
std::move(config), std::move(logs), std::move(timeKeeper), nodeIdentity);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -175,18 +175,21 @@ makeApplication(
|
||||
std::unique_ptr<TimeKeeper> timeKeeper);
|
||||
|
||||
/**
|
||||
* Construct the application with a known node public key.
|
||||
* Construct the application with a known node identity.
|
||||
*
|
||||
* Telemetry builds its resource attributes during construction and they are
|
||||
* immutable, so the base58 node public key must be supplied here. Pass
|
||||
* std::nullopt when it is unknown; that run reports no instance id. See
|
||||
* resolveNodePublicKey().
|
||||
* immutable, so the node keypair must be supplied here. See
|
||||
* resolveNodeIdentity(), which decides it from the config and command line
|
||||
* alone; setup() then persists it.
|
||||
*
|
||||
* The three-argument overload above mints a keypair, which is what a test
|
||||
* Application and a standalone run get anyway.
|
||||
*/
|
||||
std::unique_ptr<Application>
|
||||
makeApplication(
|
||||
std::unique_ptr<Config> config,
|
||||
std::unique_ptr<Logs> logs,
|
||||
std::unique_ptr<TimeKeeper> timeKeeper,
|
||||
std::optional<std::string> const& nodePublicKey);
|
||||
std::pair<PublicKey, SecretKey> const& resolvedIdentity);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -807,13 +807,14 @@ run(int argc, char** argv)
|
||||
if (vm.contains("debug"))
|
||||
setDebugLogSink(logs->makeSink("Debug", beast::Severity::Trace));
|
||||
|
||||
// Telemetry needs the node public key at construction, so read it here
|
||||
// where a config error can still be reported and the process can exit
|
||||
// cleanly. getNodeIdentity() in setup() stays authoritative.
|
||||
std::optional<std::string> nodePublicKey;
|
||||
// Telemetry stamps the node public key into resources it builds during
|
||||
// construction, so the identity is decided here, where a malformed
|
||||
// [node_seed] can still be reported and the process can exit cleanly.
|
||||
// setup() persists it; see getNodeIdentity().
|
||||
std::optional<std::pair<PublicKey, SecretKey>> nodeIdentity;
|
||||
try
|
||||
{
|
||||
nodePublicKey = resolveNodePublicKey(*config, vm, logs->journal("Application"));
|
||||
nodeIdentity = resolveNodeIdentity(*config, vm, logs->journal("Application"));
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
@@ -821,14 +822,6 @@ run(int argc, char** argv)
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!nodePublicKey)
|
||||
{
|
||||
JLOG(logs->journal("Application").warn())
|
||||
<< "Telemetry: no node identity available yet, so this run reports an empty "
|
||||
"service.instance.id. Set [telemetry] service_instance_id, or restart once "
|
||||
"the node key exists.";
|
||||
}
|
||||
|
||||
// Application construction runs member initializers that validate
|
||||
// config (for example the [telemetry] section) and can throw. A throw
|
||||
// from a member-initializer list cannot be recovered inside the
|
||||
@@ -840,14 +833,14 @@ run(int argc, char** argv)
|
||||
//
|
||||
// Only the construction is covered. The [telemetry] section is parsed
|
||||
// near the top of the member list, before the job queue and node store
|
||||
// are built, so unwinding that throw destroys very little. setup() is
|
||||
// are built, so unwinding that throw destroys little. setup() is
|
||||
// left outside deliberately: it starts subsystems whose shutdown order
|
||||
// is delicate, and only the normal stop sequence gets that order right.
|
||||
std::unique_ptr<Application> app;
|
||||
try
|
||||
{
|
||||
app = makeApplication(
|
||||
std::move(config), std::move(logs), std::make_unique<TimeKeeper>(), nodePublicKey);
|
||||
std::move(config), std::move(logs), std::make_unique<TimeKeeper>(), *nodeIdentity);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
|
||||
@@ -30,104 +30,88 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
std::pair<PublicKey, SecretKey>
|
||||
getNodeIdentity(Application& app, boost::program_options::variables_map const& cmdline)
|
||||
{
|
||||
std::optional<Seed> seed;
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* The seed a configured `[node_seed]` or `--nodeid` names.
|
||||
*
|
||||
* @param config The server configuration.
|
||||
* @param cmdline The command line parameters passed into the application.
|
||||
* @return The seed, or std::nullopt when neither is configured.
|
||||
* @throws std::runtime_error if the configured value is malformed.
|
||||
*/
|
||||
std::optional<Seed>
|
||||
configuredSeed(Config const& config, boost::program_options::variables_map const& cmdline)
|
||||
{
|
||||
if (cmdline.contains("nodeid"))
|
||||
{
|
||||
seed = parseGenericSeed(cmdline["nodeid"].as<std::string>(), false);
|
||||
|
||||
auto seed = parseGenericSeed(cmdline["nodeid"].as<std::string>(), false);
|
||||
if (!seed)
|
||||
Throw<std::runtime_error>("Invalid 'nodeid' in command line");
|
||||
return seed;
|
||||
}
|
||||
else if (app.config().exists(Sections::kNodeSeed))
|
||||
{
|
||||
seed = parseBase58<Seed>(app.config().section(Sections::kNodeSeed).lines().front());
|
||||
|
||||
if (config.exists(Sections::kNodeSeed))
|
||||
{
|
||||
auto const& lines = config.section(Sections::kNodeSeed).lines();
|
||||
auto seed = lines.empty() ? std::nullopt : parseBase58<Seed>(lines.front());
|
||||
if (!seed)
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
std::string("Invalid [") + Sections::kNodeSeed + "] in configuration file");
|
||||
}
|
||||
return seed;
|
||||
}
|
||||
|
||||
if (seed)
|
||||
{
|
||||
auto secretKey = generateSecretKey(KeyType::Secp256k1, *seed);
|
||||
auto publicKey = derivePublicKey(KeyType::Secp256k1, secretKey);
|
||||
|
||||
return {publicKey, secretKey};
|
||||
}
|
||||
|
||||
auto db = app.getWalletDB().checkoutDb();
|
||||
|
||||
if (cmdline.contains("newnodeid"))
|
||||
clearNodeIdentity(*db);
|
||||
|
||||
return getNodeIdentity(*db);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
resolveNodePublicKey(
|
||||
Config const& config,
|
||||
boost::program_options::variables_map const& cmdline,
|
||||
beast::Journal journal)
|
||||
/**
|
||||
* The keypair a seed defines.
|
||||
*
|
||||
* @param seed The configured seed.
|
||||
* @return The derived secp256k1 keypair.
|
||||
*/
|
||||
std::pair<PublicKey, SecretKey>
|
||||
keysFromSeed(Seed const& seed)
|
||||
{
|
||||
std::optional<Seed> seed;
|
||||
bool seedConfigured = false;
|
||||
|
||||
if (cmdline.contains("nodeid"))
|
||||
{
|
||||
seedConfigured = true;
|
||||
seed = parseGenericSeed(cmdline["nodeid"].as<std::string>(), false);
|
||||
}
|
||||
else if (config.exists(Sections::kNodeSeed))
|
||||
{
|
||||
seedConfigured = true;
|
||||
if (auto const& lines = config.section(Sections::kNodeSeed).lines(); !lines.empty())
|
||||
seed = parseBase58<Seed>(lines.front());
|
||||
}
|
||||
|
||||
// A configured seed decides the identity outright. A malformed or missing
|
||||
// one is reported by getNodeIdentity(), which runs later.
|
||||
if (seedConfigured)
|
||||
{
|
||||
if (!seed)
|
||||
return std::nullopt;
|
||||
|
||||
auto const secretKey = generateSecretKey(KeyType::Secp256k1, *seed);
|
||||
return toBase58(TokenType::NodePublic, derivePublicKey(KeyType::Secp256k1, secretKey));
|
||||
}
|
||||
|
||||
// --newnodeid discards whatever is stored.
|
||||
if (cmdline.contains("newnodeid"))
|
||||
return std::nullopt;
|
||||
auto const secretKey = generateSecretKey(KeyType::Secp256k1, seed);
|
||||
return {derivePublicKey(KeyType::Secp256k1, secretKey), secretKey};
|
||||
}
|
||||
|
||||
/**
|
||||
* The stored identity, read without creating or modifying anything.
|
||||
*
|
||||
* Runs before the Application, so it opens the wallet itself rather than going
|
||||
* through getWalletDB(). Three things keep that safe: the file must already
|
||||
* exist, the init SQL is empty so the schema is never created, and the global
|
||||
* pragmas are off because they include journal_mode, which rewrites the
|
||||
* database header. The connection closes before this returns.
|
||||
*
|
||||
* @param config The server configuration.
|
||||
* @param journal Journal for reporting an unreadable database.
|
||||
* @return The stored keypair, or std::nullopt when there is none to read.
|
||||
*/
|
||||
std::optional<std::pair<PublicKey, SecretKey>>
|
||||
storedIdentity(Config const& config, beast::Journal journal)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto setup = setupDatabaseCon(config, journal);
|
||||
|
||||
// Standalone uses a temporary database, so nothing is persisted and this
|
||||
// run will mint a fresh key.
|
||||
// Standalone gets a private temporary database, so there is nothing
|
||||
// persisted to read and nothing setup() could read back either.
|
||||
if (setup.standAlone && setup.startUp != StartUpType::Load &&
|
||||
setup.startUp != StartUpType::LoadFile && setup.startUp != StartUpType::Replay)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// The global pragmas include journal_mode, which rewrites the database
|
||||
// header. The wallet is opened without them everywhere else.
|
||||
setup.useGlobalPragma = false;
|
||||
|
||||
// Only read an existing file: SQLite would otherwise create one.
|
||||
if (std::error_code ec; !std::filesystem::exists(setup.dataDir / kWalletDbName, ec))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Empty init SQL: open the existing schema, never create it.
|
||||
DatabaseCon walletDb{
|
||||
setup,
|
||||
kWalletDbName,
|
||||
@@ -136,8 +120,7 @@ resolveNodePublicKey(
|
||||
journal};
|
||||
|
||||
auto db = walletDb.checkoutDb();
|
||||
if (auto const stored = readNodeIdentity(*db))
|
||||
return toBase58(TokenType::NodePublic, stored->first);
|
||||
return readNodeIdentity(*db);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
@@ -147,4 +130,59 @@ resolveNodePublicKey(
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::pair<PublicKey, SecretKey>
|
||||
resolveNodeIdentity(
|
||||
Config const& config,
|
||||
boost::program_options::variables_map const& cmdline,
|
||||
beast::Journal journal)
|
||||
{
|
||||
// A configured seed decides the identity outright, and nothing is stored.
|
||||
if (auto const seed = configuredSeed(config, cmdline))
|
||||
return keysFromSeed(*seed);
|
||||
|
||||
// --newnodeid discards whatever is stored, so mint now; getNodeIdentity()
|
||||
// clears the old row and stores this pair.
|
||||
if (!cmdline.contains("newnodeid"))
|
||||
{
|
||||
if (auto const stored = storedIdentity(config, journal))
|
||||
return *stored;
|
||||
}
|
||||
|
||||
// Nothing to read: a first boot, or a standalone run's temporary database.
|
||||
// Mint here so telemetry has an identity from construction; setup()
|
||||
// persists this pair if there is a database to hold it.
|
||||
return randomKeyPair(KeyType::Secp256k1);
|
||||
}
|
||||
|
||||
std::pair<PublicKey, SecretKey>
|
||||
getNodeIdentity(
|
||||
Application& app,
|
||||
boost::program_options::variables_map const& cmdline,
|
||||
std::pair<PublicKey, SecretKey> const& resolved)
|
||||
{
|
||||
// A configured seed reaches neither the reader nor the writer.
|
||||
if (cmdline.contains("nodeid") || app.config().exists(Sections::kNodeSeed))
|
||||
return resolved;
|
||||
|
||||
auto db = app.getWalletDB().checkoutDb();
|
||||
|
||||
if (cmdline.contains("newnodeid"))
|
||||
clearNodeIdentity(*db);
|
||||
|
||||
// What is stored wins, so a restart keeps the node's identity even if
|
||||
// another process wrote one between construction and here. Telemetry's
|
||||
// resources are already built from `resolved`, so on that one run the two
|
||||
// would disagree; it needs a restart to line up, as the configuration
|
||||
// reference records.
|
||||
if (auto const stored = readNodeIdentity(*db))
|
||||
return *stored;
|
||||
|
||||
// Nothing stored, or --newnodeid just cleared it. Persist the pair
|
||||
// telemetry is already reporting, so both agree from now on.
|
||||
storeNodeIdentity(*db, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -16,34 +16,47 @@
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* The cryptographic credentials identifying this server instance.
|
||||
* This server's identity, resolved before the Application exists.
|
||||
*
|
||||
* @param app The application object
|
||||
* @param cmdline The command line parameters passed into the application.
|
||||
*/
|
||||
std::pair<PublicKey, SecretKey>
|
||||
getNodeIdentity(Application& app, boost::program_options::variables_map const& cmdline);
|
||||
|
||||
/**
|
||||
* This server's public key, read without creating or modifying anything.
|
||||
* Telemetry stamps the node public key into resource attributes that are
|
||||
* immutable once built, and those resources are built in ApplicationImp's
|
||||
* member-init list. So the identity has to be decided before construction,
|
||||
* from the config and the command line alone.
|
||||
*
|
||||
* For callers that need the identity before the Application exists, such as
|
||||
* telemetry building its resource attributes in the member-init list. Derives
|
||||
* from a configured seed when there is one, otherwise reads the wallet database
|
||||
* only if it already exists.
|
||||
*
|
||||
* getNodeIdentity() remains authoritative and mints a key when none exists.
|
||||
* Always returns a keypair. It derives one from a configured seed, else reads
|
||||
* the wallet database if it already exists, else mints one. Nothing is created
|
||||
* or written here: getNodeIdentity() persists the result once setup() has
|
||||
* opened the database.
|
||||
*
|
||||
* @param config The server configuration.
|
||||
* @param cmdline The command line parameters passed into the application.
|
||||
* @param journal Journal for reporting an unreadable database.
|
||||
* @return The base58-encoded node public key, or std::nullopt if none can be
|
||||
* read.
|
||||
* @return This node's keypair.
|
||||
* @throws std::runtime_error if a configured seed is malformed.
|
||||
*/
|
||||
std::optional<std::string>
|
||||
resolveNodePublicKey(
|
||||
std::pair<PublicKey, SecretKey>
|
||||
resolveNodeIdentity(
|
||||
Config const& config,
|
||||
boost::program_options::variables_map const& cmdline,
|
||||
beast::Journal journal);
|
||||
|
||||
/**
|
||||
* The cryptographic credentials identifying this server instance, persisted.
|
||||
*
|
||||
* Called from setup(), once the wallet database is open. Stores @p resolved
|
||||
* when the database holds no identity, and returns whatever the database holds
|
||||
* when it does.
|
||||
*
|
||||
* @param app The application object
|
||||
* @param cmdline The command line parameters passed into the application.
|
||||
* @param resolved The keypair resolveNodeIdentity() decided before
|
||||
* construction, which telemetry is already reporting.
|
||||
* @return This node's keypair.
|
||||
*/
|
||||
std::pair<PublicKey, SecretKey>
|
||||
getNodeIdentity(
|
||||
Application& app,
|
||||
boost::program_options::variables_map const& cmdline,
|
||||
std::pair<PublicKey, SecretKey> const& resolved);
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
Reference in New Issue
Block a user