Compare commits

...

9 Commits

Author SHA1 Message Date
Denis Angell
2e5926e260 feat: Add DatagramMonitor, a UDP node-stats exporter 2026-09-17 04:00:24 -04:00
Sergey Kuznetsov
9403736199 ci: Exclude Rust unit tests from code coverage (#8203) 2026-09-10 11:38:25 +00:00
Mayukha Vadari
028783661d feat: Apply .macro changes from ripple/smart-escrow (#8157)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-09 20:05:58 +00:00
Vito Tumas
21890d9daf feat: Register featureLendingProtocolV1_2 amendment (#8185)
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-09-09 10:01:32 +00:00
Sergey Kuznetsov
060957ed39 ci: Add nightly Rust toolchain to the CI image (#8182)
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-09-09 01:24:28 +00:00
Ayaz Salikhov
1381483c7a build: Fix test installation on debian:11 due to EOL (#8200) 2026-09-08 22:49:16 +00:00
dependabot[bot]
3e4bdf2782 ci: [DEPENDABOT] bump actions/deploy-pages from 5.0.0 to 5.0.1 in the github-actions group across 1 directory (#8180)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 16:07:56 +00:00
Mayukha Vadari
e3c8996e44 feat: Add fixCleanup3_5_0 amendment placeholder (#8174) 2026-09-05 00:06:14 +00:00
yinyiqian1
d5bfe94f15 feat: Support key rotation in MPTokenIssuanceSet (#7915) 2026-09-03 21:13:06 +00:00
57 changed files with 3016 additions and 146 deletions

View File

@@ -372,6 +372,7 @@ words:
- writeme
- wsrch
- wthread
- Xahau
- xbridge
- xchain
- xcrun

View File

@@ -1,5 +1,5 @@
{
"image_tag": "sha-473fe44",
"image_tag": "sha-060957e",
"configs": {
"ubuntu": [
{

View File

@@ -5,15 +5,13 @@ on:
branches:
- develop
paths:
- ".github/workflows/build-nix-images.yml"
- "flake.nix"
- "flake.lock"
- "rust-toolchain.toml"
- "nix/**"
- "!nix/docker/README.md"
- "!nix/devshell.nix"
- "!nix/check-tools/*.txt"
- "bin/check-tools.sh"
- "!nix/check-tools/**"
- "bin/default-loader-path.sh"
- "bin/install-sanitizer-libs.sh"
pull_request:
@@ -25,7 +23,7 @@ on:
- "nix/**"
- "!nix/docker/README.md"
- "!nix/devshell.nix"
- "!nix/check-tools/*.txt"
- "!nix/check-tools/**"
- "bin/check-tools.sh"
- "bin/default-loader-path.sh"
- "bin/install-sanitizer-libs.sh"

View File

@@ -34,7 +34,7 @@ permissions:
jobs:
audit:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
permissions:
contents: read
# Needed to open an issue on scheduled failures.

View File

@@ -41,7 +41,7 @@ env:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -91,4 +91,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1

View File

@@ -34,7 +34,7 @@ jobs:
needs: [determine-files]
if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-473fe44"
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-060957e"
permissions:
contents: read
issues: write

View File

@@ -209,6 +209,23 @@ jobs:
}
echo "package=${package}" >>"${GITHUB_OUTPUT}"
# Debian 11 went end-of-life on 2026-08-31
# (https://www.debian.org/News/2026/20260831) and its packages are
# already partly gone from deb.debian.org, so switch to the
# snapshot.debian.org entries the image ships commented out in its
# sources.list: they are pinned to the snapshot the image was built
# from, so they serve every version it needs and never go away.
# Snapshots keep their original, long-passed Valid-Until, hence the
# disabled check; the retries absorb snapshot.debian.org's throttling.
- name: Switch Debian 11 to snapshot.debian.org
if: ${{ matrix.image == 'debian:11' }}
run: |
sed -i 's|^deb |# deb |; s|^# deb http://snapshot|deb http://snapshot|' /etc/apt/sources.list
printf '%s\n' \
'Acquire::Check-Valid-Until "false";' \
'Acquire::Retries "3";' \
>/etc/apt/apt.conf.d/99snapshot
- name: Install the DEB
if: ${{ matrix.package_type == 'deb' }}
env:

View File

@@ -1,8 +1,9 @@
# Clippy, coverage and documentation for the Rust crates in crates/. Each runs
# as an independent job on a GitHub-hosted runner, but inside the same container
# image used to build the crates in the C++/Corrosion path, so the toolchain
# (and therefore the lints, coverage instrumentation and the cargo cache) matches
# what production builds use.
# (and therefore the lints and the cargo cache) matches what production builds
# use. Coverage is the exception: it needs the nightly rustc that honours
# #[coverage(off)], which the image carries alongside the pinned stable.
#
# Rust unit tests are deliberately NOT run here. They run as part of the C++
# build (reusable-build-test-config.yml), which already compiles the crates on a
@@ -27,7 +28,7 @@ permissions:
jobs:
clippy:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -40,11 +41,14 @@ jobs:
coverage:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Use the nightly Rust toolchain
run: rust-nightly path >>"${GITHUB_PATH}"
- name: Use cargo artifacts cache
uses: ./.github/actions/cargo-cache
@@ -66,7 +70,7 @@ jobs:
doc:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

View File

@@ -40,7 +40,7 @@ defaults:
jobs:
upload:
runs-on: ubuntu-latest
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e
env:
REMOTE_NAME: ${{ inputs.remote_name }}
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}

View File

@@ -70,6 +70,11 @@ repos:
language: system
types: [rust]
pass_filenames: false # rustfmt formats the whole workspace
- id: check-coverage-attrs
name: check Rust coverage attributes
entry: ./bin/pre-commit/check_rust_coverage_attrs.py
language: python
files: ^crates/.*\.rs$
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7

View File

@@ -158,6 +158,7 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
check cargo-nextest cargo nextest --version
check clippy-driver
check rust-analyzer
check rust-nightly rust-nightly run rustc --version
check rustc
check rustfmt
fi

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
Check that Rust unit tests stay out of the coverage report.
cargo-llvm-cov instruments the test code along with everything else, so a test
module that is not excluded counts its own body as covered and inflates the
reported number. Excluding it takes two attributes:
* every `#[cfg(test)]` module carries
`#[cfg_attr(coverage_nightly, coverage(off))]`;
* every crate root (lib.rs, main.rs) carries
`#![cfg_attr(coverage_nightly, feature(coverage_attribute))]`, which the
attribute above needs in order to compile.
Both are inert outside the coverage job: cargo-llvm-cov defines
`coverage_nightly` only when it runs on a nightly toolchain.
The crate-root gate is checked even in a crate that has no tests yet, because
that is what lets the first test module added later carry the attribute without
a build failure. Missing it is a hard error, so it cannot go unnoticed; a
missing `coverage(off)` fails open, which is why this check exists.
Matching is on exact attribute text, which works because `cargo fmt` runs over
the whole workspace in the hook ahead of this one: rustfmt puts every attribute
on its own line and normalizes what is inside it, turning `#[cfg( test )]`
and `#[cfg(test,)]` alike into `#[cfg(test)]`. So there is nothing here that
parses Rust. The price is that a cfg this file does not spell out literally --
`all(test, ...)`, `any(test, ...)`, `not(test)` -- is reported rather than
classified, on the grounds that guessing at coverage semantics is how a check
like this ends up quietly wrong.
Usage: ./bin/pre-commit/check_rust_coverage_attrs.py <file1> <file2> ...
Exit status is non-zero if any violation is found.
"""
import re
import sys
from dataclasses import dataclass
from pathlib import Path
CRATE_ROOTS = {"lib.rs", "main.rs"}
FEATURE_ATTR = "#![cfg_attr(coverage_nightly, feature(coverage_attribute))]"
COVERAGE_OFF_ATTR = "#[cfg_attr(coverage_nightly, coverage(off))]"
CFG_TEST_ATTR = "#[cfg(test)]"
# Any other cfg that mentions `test`. String literals are blanked before this
# runs, so `feature = "test"` does not read as the `test` cfg.
RE_CFG_MENTIONS_TEST = re.compile(r"^#\[cfg\(.*\btest\b.*\)\]$")
RE_STRING = re.compile(r'"(?:[^"\\]|\\.)*"')
RE_MOD = re.compile(r"^(?:pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)")
@dataclass(frozen=True)
class Finding:
line: int
label: str
message: str
def _check_module(attrs: list[str], line: int, name: str) -> list[Finding]:
"""Findings for one module, given the attributes attached to it."""
if COVERAGE_OFF_ATTR in attrs:
return [] # excluded from coverage; which cfg gates it does not matter
if CFG_TEST_ATTR in attrs:
return [
Finding(
line,
"missing-coverage-off",
f"`mod {name}` is #[cfg(test)] but not excluded from coverage; "
f"add {COVERAGE_OFF_ATTR}",
)
]
unclassified = [
attr for attr in attrs if RE_CFG_MENTIONS_TEST.match(RE_STRING.sub('""', attr))
]
if unclassified:
return [
Finding(
line,
"unclassified-cfg",
f"`mod {name}` is gated on {unclassified[0]}, which this check "
f"cannot tell apart from a module that ships in the library; "
f"add {COVERAGE_OFF_ATTR} if it is test-only, or teach this "
f"check the cfg if it is not",
)
]
return []
def _check_test_modules(lines: list[str]) -> list[Finding]:
"""Findings for every test module that is not excluded from coverage."""
findings: list[Finding] = []
attrs: list[str] = []
attrs_line = 0
for number, raw in enumerate(lines, start=1):
stripped = raw.strip()
# Blank lines and comments are allowed between an attribute and its item.
if not stripped or stripped.startswith("//"):
continue
if stripped.startswith("#["):
if not attrs:
attrs_line = number
attrs.append(stripped)
continue
module = RE_MOD.match(stripped)
if module is not None and attrs:
findings += _check_module(attrs, attrs_line, module.group(1))
attrs = []
return findings
def _check_crate_root(name: str, lines: list[str]) -> list[Finding]:
"""A finding if a crate root is missing the coverage_attribute feature gate."""
if name not in CRATE_ROOTS:
return []
if any(line.strip() == FEATURE_ATTR for line in lines):
return []
return [
Finding(
1,
"missing-feature-gate",
f"crate root is missing {FEATURE_ATTR}",
)
]
def check_source(name: str, text: str) -> list[Finding]:
"""Findings for one file's contents; `name` is its base name (lib.rs, ...)."""
lines = text.splitlines()
return _check_crate_root(name, lines) + _check_test_modules(lines)
def check_file(path: Path) -> list[Finding]:
return check_source(path.name, path.read_text(encoding="utf-8"))
def main() -> int:
total = 0
for path in (Path(name) for name in sys.argv[1:]):
for finding in check_file(path):
total += 1
print(f"{path}:{finding.line}: {finding.label}: {finding.message}")
return 1 if total else 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -8,6 +8,9 @@ cxx = { version = "1.0.198", features = ["c++20"] }
[workspace.package]
edition = "2024"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage)', 'cfg(coverage_nightly)' ] }
[profile.release]
opt-level = 3
overflow-checks = true

View File

@@ -8,3 +8,6 @@ crate-type = ["staticlib"]
[dependencies]
cxx.workspace = true
[lints]
workspace = true

View File

@@ -1,3 +1,5 @@
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#[cxx::bridge(namespace = "rs::hello_world")]
mod ffi {
extern "Rust" {
@@ -8,3 +10,14 @@ mod ffi {
pub fn hello_world() -> String {
"hello_world".to_string()
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn hello_world_returns_hello_world() {
assert_eq!(hello_world(), "hello_world")
}
}

View File

@@ -11,6 +11,7 @@ struct Sections
static constexpr auto kCompression = "compression";
static constexpr auto kCrawl = "crawl";
static constexpr auto kDatabasePath = "database_path";
static constexpr auto kDatagramMonitor = "datagram_monitor";
static constexpr auto kDebugLogfile = "debug_logfile";
static constexpr auto kElbSupport = "elb_support";
static constexpr auto kFeatures = "features";

View File

@@ -12,6 +12,7 @@
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <limits>
namespace xrpl {
@@ -544,6 +545,11 @@ constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_
*/
constexpr std::uint32_t kConfidentialFeeMultiplier = 9;
/**
* Maximum value a confidential MPT key epoch may reach.
*/
constexpr std::uint32_t kMaxKeyEpoch = std::numeric_limits<std::uint32_t>::max();
/**
* Compressed EC point prefix for even y-coordinate
*/

View File

@@ -129,8 +129,11 @@ enum TEMcodes : TERUnderlyingType {
temARRAY_TOO_LARGE,
temBAD_TRANSFER_FEE,
temINVALID_INNER_BATCH,
temBAD_MPT,
temBAD_CIPHERTEXT,
temINVALID_BYTECODE,
temTEMP_DISABLED,
};
//------------------------------------------------------------------------------
@@ -179,6 +182,8 @@ enum TEFcodes : TERUnderlyingType {
tefINVALID_LEDGER_FIX_TYPE,
tefNO_DST_PARTIAL,
tefBAD_PATH_COUNT,
tefNO_BYTECODE,
tefBYTECODE_NOT_INCLUDED,
};
//------------------------------------------------------------------------------
@@ -370,6 +375,8 @@ enum TECcodes : TERUnderlyingType {
tecNO_DELEGATE_PERMISSION = 198,
tecBAD_PROOF = 199,
tecNO_SPONSOR_PERMISSION = 200,
tecOUT_OF_GAS = 201,
tecBYTECODE_REJECTED = 202,
};
//------------------------------------------------------------------------------

View File

@@ -15,6 +15,10 @@
// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo)

View File

@@ -309,6 +309,11 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({
{sfBaseFeeDrops, SoeOptional},
{sfReserveBaseDrops, SoeOptional},
{sfReserveIncrementDrops, SoeOptional},
// Smart Escrow fields
{sfGasLimit, SoeOptional},
{sfBytecodeSizeLimit, SoeOptional},
{sfGasPrice, SoeOptional},
{sfPreviousTxnID, SoeOptional},
{sfPreviousTxnLgrSeq, SoeOptional},
}))
@@ -339,6 +344,8 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({
{sfCondition, SoeOptional},
{sfCancelAfter, SoeOptional},
{sfFinishAfter, SoeOptional},
{sfBytecode, SoeOptional},
{sfData, SoeOptional},
{sfSourceTag, SoeOptional},
{sfDestinationTag, SoeOptional},
{sfOwnerNode, SoeRequired},
@@ -408,6 +415,8 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({
{sfReferenceHolding, SoeOptional},
{sfIssuerEncryptionKey, SoeOptional},
{sfAuditorEncryptionKey, SoeOptional},
{sfIssuerKeyEpoch, SoeOptional},
{sfAuditorKeyEpoch, SoeOptional},
{sfConfidentialOutstandingAmount, SoeDefault},
}))

View File

@@ -119,6 +119,15 @@ TYPED_SFIELD(sfRemainingOwnerCount, UINT32, 73)
TYPED_SFIELD(sfSponsorFlags, UINT32, 74)
TYPED_SFIELD(sfSubscriptionDate, UINT32, 75)
TYPED_SFIELD(sfRedemptionDate, UINT32, 76)
TYPED_SFIELD(sfIssuerKeyEpoch, UINT32, 77)
TYPED_SFIELD(sfAuditorKeyEpoch, UINT32, 78)
TYPED_SFIELD(sfIssuerKeyMirrorEpoch, UINT32, 79)
TYPED_SFIELD(sfAuditorKeyMirrorEpoch, UINT32, 80)
TYPED_SFIELD(sfGasLimit, UINT32, 81)
TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82)
TYPED_SFIELD(sfGasPrice, UINT32, 83)
TYPED_SFIELD(sfGas, UINT32, 84)
TYPED_SFIELD(sfGasUsed, UINT32, 85)
// 64-bit integers (common)
TYPED_SFIELD(sfIndexNext, UINT64, 1)
@@ -234,6 +243,7 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset
// 32-bit signed (common)
TYPED_SFIELD(sfLoanScale, INT32, 1)
TYPED_SFIELD(sfRemainingOwnerCountDelta, INT32, 2)
TYPED_SFIELD(sfVMReturnCode, INT32, 3)
// currency amount (common)
TYPED_SFIELD(sfAmount, AMOUNT, 1)
@@ -318,6 +328,7 @@ TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43)
TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44)
TYPED_SFIELD(sfAmountCommitment, VL, 45)
TYPED_SFIELD(sfBalanceCommitment, VL, 46)
TYPED_SFIELD(sfBytecode, VL, 47)
// account (common)
TYPED_SFIELD(sfAccount, ACCOUNT, 1)

View File

@@ -66,11 +66,13 @@ TRANSACTION(ttPAYMENT, 0, Payment,
#endif
TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({.delegable = Delegation::Delegable}), ({
{sfDestination, SoeRequired},
{sfDestinationTag, SoeOptional},
{sfAmount, SoeRequired, SoeMptSupported},
{sfCondition, SoeOptional},
{sfCancelAfter, SoeOptional},
{sfFinishAfter, SoeOptional},
{sfDestinationTag, SoeOptional},
{sfBytecode, SoeOptional},
{sfData, SoeOptional},
}))
/** This transaction type completes an existing escrow. */
@@ -83,6 +85,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({.delegable = Delegation::Delegab
{sfFulfillment, SoeOptional},
{sfCondition, SoeOptional},
{sfCredentialIDs, SoeOptional},
{sfGas, SoeOptional},
}))
@@ -1161,6 +1164,10 @@ TRANSACTION(ttFEE, 101, SetFee,
{sfBaseFeeDrops, SoeOptional},
{sfReserveBaseDrops, SoeOptional},
{sfReserveIncrementDrops, SoeOptional},
// Smart Escrow fields
{sfGasLimit, SoeOptional},
{sfBytecodeSizeLimit, SoeOptional},
{sfGasPrice, SoeOptional},
}))
/** This system-generated transaction type is used to update the network's negative UNL

View File

@@ -174,6 +174,54 @@ public:
return this->sle_->isFieldPresent(sfFinishAfter);
}
/**
* @brief Get sfBytecode (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getBytecode() const
{
if (hasBytecode())
return this->sle_->at(sfBytecode);
return std::nullopt;
}
/**
* @brief Check if sfBytecode is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasBytecode() const
{
return this->sle_->isFieldPresent(sfBytecode);
}
/**
* @brief Get sfData (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getData() const
{
if (hasData())
return this->sle_->at(sfData);
return std::nullopt;
}
/**
* @brief Check if sfData is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasData() const
{
return this->sle_->isFieldPresent(sfData);
}
/**
* @brief Get sfSourceTag (SoeOptional)
* @return The field value, or std::nullopt if not present.
@@ -453,6 +501,28 @@ public:
return *this;
}
/**
* @brief Set sfBytecode (SoeOptional)
* @return Reference to this builder for method chaining.
*/
EscrowBuilder&
setBytecode(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfBytecode] = value;
return *this;
}
/**
* @brief Set sfData (SoeOptional)
* @return Reference to this builder for method chaining.
*/
EscrowBuilder&
setData(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfData] = value;
return *this;
}
/**
* @brief Set sfSourceTag (SoeOptional)
* @return Reference to this builder for method chaining.

View File

@@ -213,6 +213,78 @@ public:
return this->sle_->isFieldPresent(sfReserveIncrementDrops);
}
/**
* @brief Get sfGasLimit (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getGasLimit() const
{
if (hasGasLimit())
return this->sle_->at(sfGasLimit);
return std::nullopt;
}
/**
* @brief Check if sfGasLimit is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasGasLimit() const
{
return this->sle_->isFieldPresent(sfGasLimit);
}
/**
* @brief Get sfBytecodeSizeLimit (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getBytecodeSizeLimit() const
{
if (hasBytecodeSizeLimit())
return this->sle_->at(sfBytecodeSizeLimit);
return std::nullopt;
}
/**
* @brief Check if sfBytecodeSizeLimit is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasBytecodeSizeLimit() const
{
return this->sle_->isFieldPresent(sfBytecodeSizeLimit);
}
/**
* @brief Get sfGasPrice (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getGasPrice() const
{
if (hasGasPrice())
return this->sle_->at(sfGasPrice);
return std::nullopt;
}
/**
* @brief Check if sfGasPrice is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasGasPrice() const
{
return this->sle_->isFieldPresent(sfGasPrice);
}
/**
* @brief Get sfPreviousTxnID (SoeOptional)
* @return The field value, or std::nullopt if not present.
@@ -375,6 +447,39 @@ public:
return *this;
}
/**
* @brief Set sfGasLimit (SoeOptional)
* @return Reference to this builder for method chaining.
*/
FeeSettingsBuilder&
setGasLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfGasLimit] = value;
return *this;
}
/**
* @brief Set sfBytecodeSizeLimit (SoeOptional)
* @return Reference to this builder for method chaining.
*/
FeeSettingsBuilder&
setBytecodeSizeLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfBytecodeSizeLimit] = value;
return *this;
}
/**
* @brief Set sfGasPrice (SoeOptional)
* @return Reference to this builder for method chaining.
*/
FeeSettingsBuilder&
setGasPrice(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfGasPrice] = value;
return *this;
}
/**
* @brief Set sfPreviousTxnID (SoeOptional)
* @return Reference to this builder for method chaining.

View File

@@ -351,6 +351,54 @@ public:
return this->sle_->isFieldPresent(sfAuditorEncryptionKey);
}
/**
* @brief Get sfIssuerKeyEpoch (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getIssuerKeyEpoch() const
{
if (hasIssuerKeyEpoch())
return this->sle_->at(sfIssuerKeyEpoch);
return std::nullopt;
}
/**
* @brief Check if sfIssuerKeyEpoch is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasIssuerKeyEpoch() const
{
return this->sle_->isFieldPresent(sfIssuerKeyEpoch);
}
/**
* @brief Get sfAuditorKeyEpoch (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getAuditorKeyEpoch() const
{
if (hasAuditorKeyEpoch())
return this->sle_->at(sfAuditorKeyEpoch);
return std::nullopt;
}
/**
* @brief Check if sfAuditorKeyEpoch is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorKeyEpoch() const
{
return this->sle_->isFieldPresent(sfAuditorKeyEpoch);
}
/**
* @brief Get sfConfidentialOutstandingAmount (SoeDefault)
* @return The field value, or std::nullopt if not present.
@@ -600,6 +648,28 @@ public:
return *this;
}
/**
* @brief Set sfIssuerKeyEpoch (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceBuilder&
setIssuerKeyEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfIssuerKeyEpoch] = value;
return *this;
}
/**
* @brief Set sfAuditorKeyEpoch (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenIssuanceBuilder&
setAuditorKeyEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfAuditorKeyEpoch] = value;
return *this;
}
/**
* @brief Set sfConfidentialOutstandingAmount (SoeDefault)
* @return Reference to this builder for method chaining.

View File

@@ -58,6 +58,32 @@ public:
return this->tx_->at(sfDestination);
}
/**
* @brief Get sfDestinationTag (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getDestinationTag() const
{
if (hasDestinationTag())
{
return this->tx_->at(sfDestinationTag);
}
return std::nullopt;
}
/**
* @brief Check if sfDestinationTag is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasDestinationTag() const
{
return this->tx_->isFieldPresent(sfDestinationTag);
}
/**
* @brief Get sfAmount (SoeRequired)
* @note This field supports MPT (Multi-Purpose Token) amounts.
@@ -149,29 +175,55 @@ public:
}
/**
* @brief Get sfDestinationTag (SoeOptional)
* @brief Get sfBytecode (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getDestinationTag() const
protocol_autogen::Optional<SF_VL::type::value_type>
getBytecode() const
{
if (hasDestinationTag())
if (hasBytecode())
{
return this->tx_->at(sfDestinationTag);
return this->tx_->at(sfBytecode);
}
return std::nullopt;
}
/**
* @brief Check if sfDestinationTag is present.
* @brief Check if sfBytecode is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasDestinationTag() const
hasBytecode() const
{
return this->tx_->isFieldPresent(sfDestinationTag);
return this->tx_->isFieldPresent(sfBytecode);
}
/**
* @brief Get sfData (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getData() const
{
if (hasData())
{
return this->tx_->at(sfData);
}
return std::nullopt;
}
/**
* @brief Check if sfData is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasData() const
{
return this->tx_->isFieldPresent(sfData);
}
};
@@ -232,6 +284,17 @@ public:
return *this;
}
/**
* @brief Set sfDestinationTag (SoeOptional)
* @return Reference to this builder for method chaining.
*/
EscrowCreateBuilder&
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfDestinationTag] = value;
return *this;
}
/**
* @brief Set sfAmount (SoeRequired)
* @note This field supports MPT (Multi-Purpose Token) amounts.
@@ -278,13 +341,24 @@ public:
}
/**
* @brief Set sfDestinationTag (SoeOptional)
* @brief Set sfBytecode (SoeOptional)
* @return Reference to this builder for method chaining.
*/
EscrowCreateBuilder&
setDestinationTag(std::decay_t<typename SF_UINT32::type::value_type> const& value)
setBytecode(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfDestinationTag] = value;
object_[sfBytecode] = value;
return *this;
}
/**
* @brief Set sfData (SoeOptional)
* @return Reference to this builder for method chaining.
*/
EscrowCreateBuilder&
setData(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfData] = value;
return *this;
}

View File

@@ -146,6 +146,32 @@ public:
{
return this->tx_->isFieldPresent(sfCredentialIDs);
}
/**
* @brief Get sfGas (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getGas() const
{
if (hasGas())
{
return this->tx_->at(sfGas);
}
return std::nullopt;
}
/**
* @brief Check if sfGas is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasGas() const
{
return this->tx_->isFieldPresent(sfGas);
}
};
/**
@@ -249,6 +275,17 @@ public:
return *this;
}
/**
* @brief Set sfGas (SoeOptional)
* @return Reference to this builder for method chaining.
*/
EscrowFinishBuilder&
setGas(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfGas] = value;
return *this;
}
/**
* @brief Build and return the EscrowFinish wrapper.
* @param publicKey The public key for signing.

View File

@@ -254,6 +254,84 @@ public:
{
return this->tx_->isFieldPresent(sfReserveIncrementDrops);
}
/**
* @brief Get sfGasLimit (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getGasLimit() const
{
if (hasGasLimit())
{
return this->tx_->at(sfGasLimit);
}
return std::nullopt;
}
/**
* @brief Check if sfGasLimit is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasGasLimit() const
{
return this->tx_->isFieldPresent(sfGasLimit);
}
/**
* @brief Get sfBytecodeSizeLimit (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getBytecodeSizeLimit() const
{
if (hasBytecodeSizeLimit())
{
return this->tx_->at(sfBytecodeSizeLimit);
}
return std::nullopt;
}
/**
* @brief Check if sfBytecodeSizeLimit is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasBytecodeSizeLimit() const
{
return this->tx_->isFieldPresent(sfBytecodeSizeLimit);
}
/**
* @brief Get sfGasPrice (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getGasPrice() const
{
if (hasGasPrice())
{
return this->tx_->at(sfGasPrice);
}
return std::nullopt;
}
/**
* @brief Check if sfGasPrice is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasGasPrice() const
{
return this->tx_->isFieldPresent(sfGasPrice);
}
};
/**
@@ -386,6 +464,39 @@ public:
return *this;
}
/**
* @brief Set sfGasLimit (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SetFeeBuilder&
setGasLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfGasLimit] = value;
return *this;
}
/**
* @brief Set sfBytecodeSizeLimit (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SetFeeBuilder&
setBytecodeSizeLimit(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfBytecodeSizeLimit] = value;
return *this;
}
/**
* @brief Set sfGasPrice (SoeOptional)
* @return Reference to this builder for method chaining.
*/
SetFeeBuilder&
setGasPrice(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfGasPrice] = value;
return *this;
}
/**
* @brief Build and return the SetFee wrapper.
* @param publicKey The public key for signing.

View File

@@ -12,6 +12,7 @@
#include <boost/asio.hpp>
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
@@ -19,6 +20,7 @@
#include <optional>
#include <sstream>
#include <string>
#include <tuple>
namespace xrpl {
@@ -83,6 +85,18 @@ class NetworkOPs : public InfoSub::Source
public:
using clock_type = beast::AbstractClock<std::chrono::steady_clock>;
// Snapshot of per-operating-mode accounting, exposed for the datagram monitor.
struct AccountingCounter
{
std::uint64_t transitions{0};
std::chrono::microseconds dur{std::chrono::microseconds(0)};
};
using StateAccountingData = std::tuple<
std::array<AccountingCounter, 5>,
OperatingMode,
std::chrono::steady_clock::time_point,
std::uint64_t>;
enum class FailHard : unsigned char { No, Yes };
static FailHard
doFailHard(bool noMeansDont)
@@ -103,6 +117,8 @@ public:
[[nodiscard]] virtual OperatingMode
getOperatingMode() const = 0;
[[nodiscard]] virtual StateAccountingData
getStateAccountingData() = 0;
[[nodiscard]] virtual std::string
strOperatingMode(OperatingMode const mode, bool const admin = false) const = 0;
[[nodiscard]] virtual std::string
@@ -220,6 +236,12 @@ public:
virtual json::Value
getConsensusInfo() = 0;
// Proposers and round time of the last consensus round, for out-of-band
// telemetry (DatagramMonitor) that cannot reach the private consensus object.
[[nodiscard]] virtual std::size_t
getPrevProposers() const = 0;
[[nodiscard]] virtual std::chrono::milliseconds
getPrevRoundTime() const = 0;
virtual json::Value
getServerInfo(bool human, bool admin, bool counters) = 0;
virtual void

View File

@@ -131,6 +131,9 @@ Rust toolchain:
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f4 2026-07-14)
/nix/store/j6apc5pmd0giy15da9p650r8zklslmvi-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/fqpjz4l0nsnji8b2pz57mnj0akbp6hcl-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc
@@ -140,4 +143,4 @@ Rust toolchain:
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
✅ All 44 checked tools are present and runnable.
✅ All 45 checked tools are present and runnable.

View File

@@ -131,6 +131,9 @@ Rust toolchain:
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
/nix/store/lr3m97p3hx1k22a7c44pb0wa7rbayhfi-rust-analyzer-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/j7kf7a5h4xypzp6x1skg4dsdx2k4fwb3-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/rustc
@@ -168,4 +171,4 @@ Mold:
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
✅ All 52 checked tools are present and runnable.
✅ All 53 checked tools are present and runnable.

View File

@@ -131,6 +131,9 @@ Rust toolchain:
✅ rust-analyzer
rust-analyzer 1.97.1 (8bab26f 2026-07-14)
/nix/store/262830dlw2517lnagfx7i7agqgl4fmsd-rust-analyzer-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rust-analyzer
✅ rust-nightly
rustc 1.99.0-nightly (87e5904f5 2026-07-20)
/nix/store/c59pxk1yikdlf129qwyg4fplmxcrha0k-rust-nightly/bin/rust-nightly
✅ rustc
rustc 1.97.1 (8bab26f4f 2026-07-14)
/nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/rustc
@@ -168,4 +171,4 @@ Mold:
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
✅ All 52 checked tools are present and runnable.
✅ All 53 checked tools are present and runnable.

View File

@@ -10,6 +10,8 @@ RUN mkdir -p ~/.config/nix && \
COPY nix/ci-env.nix /tmp/build/nix/ci-env.nix
COPY nix/linux.nix /tmp/build/nix/linux.nix
COPY nix/packages.nix /tmp/build/nix/packages.nix
COPY nix/rust-nightly.sh /tmp/build/nix/rust-nightly.sh
COPY nix/rust.nix /tmp/build/nix/rust.nix
COPY nix/utils.nix /tmp/build/nix/utils.nix
COPY flake.nix /tmp/build/
COPY flake.lock /tmp/build/

View File

@@ -16,23 +16,7 @@ let
exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@"
'';
# rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so
# cargo has a linker). That default may be different from the clang we pin here,
# so it shadows our clang and the build can silently use a different compiler
# version. Drop that cc from every propagation channel instead of pinning a
# replacement: the toolchain then carries no compiler and cargo just uses the
# active shell's stdenv cc. Must cover all channels — rust-overlay uses both
# propagatedBuildInputs and depsHostHostPropagated.
rustToolchainBase = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
rustToolchain =
let
defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv
withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath);
in
rustToolchainBase.overrideAttrs (old: {
propagatedBuildInputs = withoutDefaultCc (old.propagatedBuildInputs or [ ]);
depsHostHostPropagated = withoutDefaultCc (old.depsHostHostPropagated or [ ]);
});
rust = import ./rust.nix { inherit pkgs; };
# Nix wraps its toolchain so that binaries are exposed only under unsuffixed
# names (gcc, g++, clang-tidy, ...). Several tools probe for a
@@ -108,41 +92,38 @@ in
mkGcov
;
commonPackages = with pkgs; [
clangToolLinks
runClangTidyLink
ccache
clangbuildanalyzer
clangTools
cmake
conan
curlMinimal # needed for codecov/codecov-action
doxygen
file # needed for cpack in Clio
gcovr
gh
git
git-cliff
git-lfs
gnumake
gnupg # needed for signing commits & codecov/codecov-action
graphviz
less # needed for git diff
mold
nettools # provides netstat, used to debug failures in CI
ninja
patchelf
perl # needed for openssl
pkg-config
pre-commit
python3
runClangTidy
vim
zip
# Rust packages
cargo-audit
cargo-llvm-cov
cargo-nextest
rustToolchain
];
commonPackages =
(with pkgs; [
clangToolLinks
runClangTidyLink
ccache
clangbuildanalyzer
clangTools
cmake
conan
curlMinimal # needed for codecov/codecov-action
doxygen
file # needed for cpack in Clio
gcovr
gh
git
git-cliff
git-lfs
gnumake
gnupg # needed for signing commits & codecov/codecov-action
graphviz
less # needed for git diff
mold
nettools # provides netstat, used to debug failures in CI
ninja
patchelf
perl # needed for openssl
pkg-config
pre-commit
python3
runClangTidy
vim
zip
])
++ rust.packages;
}

23
nix/rust-nightly.sh Normal file
View File

@@ -0,0 +1,23 @@
#!@runtimeShell@
# Reaches the nightly Rust toolchain, which is deliberately kept off PATH.
# Packaged by nix/rust.nix, which explains why.
set -euo pipefail
usage() {
echo "usage: rust-nightly (path | run <command>...)" >&2
exit 2
}
case "${1-}" in
path) printf '%s\n' "@rustNightlyBin@" ;;
run)
shift
if [[ $# -eq 0 ]]; then
usage
fi
export PATH="@rustNightlyBin@:${PATH}"
exec "$@"
;;
*) usage ;;
esac

84
nix/rust.nix Normal file
View File

@@ -0,0 +1,84 @@
# The Rust half of the tool set shared by the CI environment and the dev shell:
# the stable toolchain pinned by rust-toolchain.toml, the nightly the Rust
# coverage job needs, and the cargo plugins. Consumed by packages.nix.
{ pkgs }:
let
# rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so
# cargo has a linker). That default may be different from the clang we pin
# elsewhere, so it shadows our clang and the build can silently use a different
# compiler version. Drop that cc from every propagation channel instead of
# pinning a replacement: the toolchain then carries no compiler and cargo just
# uses the active shell's stdenv cc.
#
# The channel list is every list mkDerivation propagates to a dependent's
# environment (including the two legacy aliases). rust-overlay currently only
# uses propagatedBuildInputs and depsHostHostPropagated, but covering all of
# them means an upstream switch to another channel cannot quietly put the
# compiler back on PATH.
dropDefaultCc =
toolchain:
let
defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv
withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath);
in
toolchain.overrideAttrs (
old:
pkgs.lib.genAttrs [
"depsBuildBuildPropagated"
"propagatedNativeBuildInputs" # alias of depsBuildHostPropagated
"depsBuildTargetPropagated"
"depsHostHostPropagated"
"propagatedBuildInputs" # alias of depsHostTargetPropagated
"depsTargetTargetPropagated"
] (channel: withoutDefaultCc (old.${channel} or [ ]))
);
rustToolchain = dropDefaultCc (pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml);
# cargo-llvm-cov honours the #[coverage(off)] that keeps unit tests out of the
# coverage report only under a nightly rustc, and looks for llvm-profdata and
# llvm-cov in that same toolchain's sysroot — hence llvm-tools-preview.
#
# Not every nightly ships every component, so `nightly.latest` breaks on the
# days llvm-tools-preview is absent; selectLatestNightlyWith walks back to the
# newest one that has it. The result is the newest such nightly *known to the
# locked rust-overlay*, which means updating flake.lock moves the compiler that
# produces the coverage numbers — and with it the rustc version recorded in
# nix/check-tools/*.txt, so those snapshots need regenerating alongside.
rustNightly = dropDefaultCc (
pkgs.rust-bin.selectLatestNightlyWith (
toolchain: toolchain.minimal.override { extensions = [ "llvm-tools-preview" ]; }
)
);
# A second toolchain cannot go on PATH: its cargo and rustc would collide with
# the pinned stable's in the ci-env buildEnv, which resolves collisions by
# picking one silently. Reaching the nightly only through this wrapper keeps it
# in the image closure (the Docker build copies the whole closure, not just
# what is linked into /bin) while leaving it inactive everywhere that does not
# ask for it.
#
# The script's `path` subcommand exists for scopes wider than one command — a
# CI job appending to $GITHUB_PATH, so that the cargo cache action's own
# `rustc -vV` probe, which runs in a step of its own, agrees with the toolchain
# the build will use.
rustNightlyScript = pkgs.replaceVarsWith {
name = "rust-nightly";
src = ./rust-nightly.sh;
dir = "bin";
isExecutable = true;
replacements = {
inherit (pkgs) runtimeShell;
rustNightlyBin = "${rustNightly}/bin";
};
};
in
{
packages = [
pkgs.cargo-audit
pkgs.cargo-llvm-cov
pkgs.cargo-nextest
rustNightlyScript
rustToolchain
];
}

View File

@@ -59,6 +59,10 @@ STValidation::validationFormat()
{sfBaseFeeDrops, SoeOptional},
{sfReserveBaseDrops, SoeOptional},
{sfReserveIncrementDrops, SoeOptional},
// featureSmartEscrow
{sfGasLimit, SoeOptional},
{sfBytecodeSizeLimit, SoeOptional},
{sfGasPrice, SoeOptional},
};
// clang-format on

View File

@@ -108,6 +108,8 @@ transResults()
MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."),
MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"),
MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."),
MAKE_ERROR(tecOUT_OF_GAS, "The WASM code ran out of gas during execution."),
MAKE_ERROR(tecBYTECODE_REJECTED, "The custom WASM code that was run rejected your transaction."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
@@ -133,6 +135,8 @@ transResults()
MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."),
MAKE_ERROR(tefNO_DST_PARTIAL, "Partial payment to create account not allowed."),
MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."),
MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."),
MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."),
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
@@ -204,6 +208,8 @@ transResults()
MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."),
MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."),
MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."),
MAKE_ERROR(temINVALID_BYTECODE, "Malformed: Provided byte code is invalid."),
MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."),
MAKE_ERROR(terRETRY, "Retry transaction."),
MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."),

View File

@@ -88,8 +88,13 @@ EscrowCreate::checkExtraFeatures(PreflightContext const& ctx)
// Only require featureMPTokensV1 when the escrow amount is an MPT and
// fixCleanup3_2_0 is active; XRP/IOU escrows are unaffected by this gate.
if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds<MPTIssue>())
return ctx.rules.enabled(featureMPTokensV1);
return true;
{
if (!ctx.rules.enabled(featureMPTokensV1))
return false;
}
return (!ctx.tx.isFieldPresent(sfBytecode) && !ctx.tx.isFieldPresent(sfData)) ||
ctx.rules.enabled(featureSmartEscrow);
}
template <ValidIssueType T>

View File

@@ -65,7 +65,13 @@ checkCondition(Slice f, Slice c)
bool
EscrowFinish::checkExtraFeatures(PreflightContext const& ctx)
{
return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials);
if (ctx.tx.isFieldPresent(sfCredentialIDs) && !ctx.rules.enabled(featureCredentials))
return false;
if (ctx.tx.isFieldPresent(sfGas) && !ctx.rules.enabled(featureSmartEscrow))
return false;
return true;
}
NotTEC

View File

@@ -123,6 +123,12 @@ Change::preclaim(PreclaimContext const& ctx)
ctx.tx.isFieldPresent(sfReserveIncrementDrops))
return temDISABLED;
}
// The ttFEE transaction format defines these fields as optional,
// but they are unconditionally forbidden until FeeVoteImpl is
// updated to populate them (SmartEscrow behavioral port).
if (ctx.tx.isFieldPresent(sfGasLimit) || ctx.tx.isFieldPresent(sfBytecodeSizeLimit) ||
ctx.tx.isFieldPresent(sfGasPrice))
return temDISABLED;
return tesSUCCESS;
case ttAMENDMENT:
case ttUNL_MODIFY:

View File

@@ -119,7 +119,15 @@ MPTokenIssuanceSet::preflight(PreflightContext const& ctx)
if (hasHolder && (hasIssuerElGamalKey || hasAuditorElGamalKey))
return temMALFORMED;
if (hasAuditorElGamalKey && !hasIssuerElGamalKey)
// Pre-ConfidentialMPTKeyRotation amendment, the auditor key could not be
// registered independently of the issuer key. The issuer could either:
// - Register only the issuer key (in which case an auditor key could not be added later), or
// - Register both the issuer and auditor keys simultaneously.
//
// Post-ConfidentialMPTKeyRotation amendment, the auditor key can be
// registered after the issuer key has already been registered.
if (hasAuditorElGamalKey && !hasIssuerElGamalKey &&
!ctx.rules.enabled(featureConfidentialMPTKeyRotation))
return temMALFORMED;
if (hasIssuerElGamalKey && !isValidCompressedECPoint(ctx.tx[sfIssuerEncryptionKey]))
@@ -219,18 +227,57 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
// cannot update issuer public key
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey))
{
return tecNO_PERMISSION;
}
// Updating an existing encryption key requires the
// ConfidentialMPTKeyRotation amendment.
bool const canRotateKey = ctx.view.rules().enabled(featureConfidentialMPTKeyRotation);
// cannot update auditor public key
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey))
bool const txHasIssuerKey = ctx.tx.isFieldPresent(sfIssuerEncryptionKey);
bool const txHasAuditorKey = ctx.tx.isFieldPresent(sfAuditorEncryptionKey);
bool const sleHasIssuerKey = sleMptIssuance->isFieldPresent(sfIssuerEncryptionKey);
bool const sleHasAuditorKey = sleMptIssuance->isFieldPresent(sfAuditorEncryptionKey);
if (canRotateKey)
{
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// Post-ConfidentialMPTKeyRotation amendment, the encryption keys can be updated.
// A first-time auditor key registration requires an issuer key,
// either already on the issuance or set by the same transaction.
bool const registersAuditorKey = txHasAuditorKey && !sleHasAuditorKey;
bool const issuerKeyExists = sleHasIssuerKey || txHasIssuerKey;
if (registersAuditorKey && !issuerKeyExists)
return tecNO_PERMISSION;
// Rotating a key to its current value is not permitted: a key epoch
// increment must always correspond to an actual key change.
if (txHasIssuerKey && sleHasIssuerKey &&
ctx.tx[sfIssuerEncryptionKey] == (*sleMptIssuance)[sfIssuerEncryptionKey])
return tecDUPLICATE;
if (txHasAuditorKey && sleHasAuditorKey &&
ctx.tx[sfAuditorEncryptionKey] == (*sleMptIssuance)[sfAuditorEncryptionKey])
return tecDUPLICATE;
// Key epochs must never wrap. Epoch 0 serves as the sentinel for "never
// rotated." Holders' mirror epochs are checked against it for equality,
// so a wrap would cause stale mirror ciphertexts to appear valid instead
// of failing loudly.
if (txHasIssuerKey && sleHasIssuerKey &&
(*sleMptIssuance)[~sfIssuerKeyEpoch].value_or(0) == kMaxKeyEpoch)
return tecNO_PERMISSION;
if (txHasAuditorKey && sleHasAuditorKey &&
(*sleMptIssuance)[~sfAuditorKeyEpoch].value_or(0) == kMaxKeyEpoch)
return tecNO_PERMISSION;
}
else
{
// Pre-ConfidentialMPTKeyRotation amendment, the encryption keys can not be updated.
// cannot update issuer public key
if (txHasIssuerKey && sleHasIssuerKey)
return tecNO_PERMISSION;
// cannot update auditor public key
if (txHasAuditorKey && sleHasAuditorKey)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}
auto const enablesConfidentialBalance =
@@ -241,25 +288,30 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
// Encryption keys can only be set if confidential amounts are already
// enabled on the issuance OR if the transaction is enabling it
if (ctx.tx.isFieldPresent(sfIssuerEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialBalance)
if (txHasIssuerKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
!enablesConfidentialBalance)
{
return tecNO_PERMISSION;
}
if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) &&
!sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) && !enablesConfidentialBalance)
if (txHasAuditorKey && !sleMptIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) &&
!enablesConfidentialBalance)
{
return tecNO_PERMISSION;
}
// cannot upload key if there's circulating supply of COA
if ((ctx.tx.isFieldPresent(sfIssuerEncryptionKey) ||
ctx.tx.isFieldPresent(sfAuditorEncryptionKey) || enablesConfidentialBalance) &&
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0)
{
bool const hasConfidentialOA =
(*sleMptIssuance)[~sfConfidentialOutstandingAmount].value_or(0) > 0;
// Pre-ConfidentialMPTKeyRotation amendment, keys cannot be uploaded while
// COA > 0. Post-amendment they can be uploaded even if COA > 0.
if (!canRotateKey && (txHasIssuerKey || txHasAuditorKey) && hasConfidentialOA)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
}
// Enabling confidential balances when COA > 0 is not permitted, regardless of
// ConfidentialMPTKeyRotation.
if (enablesConfidentialBalance && hasConfidentialOA)
return tecNO_PERMISSION;
return tesSUCCESS;
}
@@ -377,25 +429,69 @@ MPTokenIssuanceSet::doApply()
}
}
if (auto const pubKey = ctx_.tx[~sfIssuerEncryptionKey])
{
// This is enforced in preflight.
// Sets an encryption key on the issuance. Overwriting an existing key
// (a rotation) increments the corresponding key epoch; a first-time
// registration leaves the epoch absent (epoch 0), matching issuances
// whose keys were registered before the ConfidentialMPTKeyRotation
// amendment.
bool const canRotateKey = view().rules().enabled(featureConfidentialMPTKeyRotation);
auto const setEncryptionKey = [&](SF_VL const& keyField, SF_UINT32 const& epochField) -> TER {
auto const pubKey = ctx_.tx[~keyField];
if (!pubKey)
return tesSUCCESS;
// This is enforced in preflight, which rejects a transaction carrying
// both sfHolder and an encryption key.
XRPL_ASSERT(
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
sle->setFieldVL(sfIssuerEncryptionKey, *pubKey);
}
// Add sanity check under the amendment ConfidentialMPTKeyRotation.
// Pre-confidentialMPTKeyRotation did not return tecINTERNAL so
// this should be under the amendment guard.
if (canRotateKey && sle->getType() != ltMPTOKEN_ISSUANCE)
return tecINTERNAL; // LCOV_EXCL_LINE
if (auto const pubKey = ctx_.tx[~sfAuditorEncryptionKey])
{
// This is enforced in preflight.
XRPL_ASSERT(
sle->getType() == ltMPTOKEN_ISSUANCE,
"MPTokenIssuanceSet::doApply : modifying MPTokenIssuance");
// NOTE: presence must be checked before the key is overwritten below.
bool const isRotation = sle->isFieldPresent(keyField);
sle->setFieldVL(keyField, *pubKey);
sle->setFieldVL(sfAuditorEncryptionKey, *pubKey);
}
if (isRotation)
{
// Preclaim rejects overwriting an existing key unless the amendment is
// enabled.
if (!canRotateKey)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::MPTokenIssuanceSet::doApply : rotation without amendment");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
auto const epoch = (*sle)[~epochField].valueOr(0);
// Preclaim rejects a rotation that would wrap the epoch. So this should never happen.
if (epoch >= kMaxKeyEpoch)
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::MPTokenIssuanceSet::doApply : key epoch overflow");
return tecINTERNAL;
// LCOV_EXCL_STOP
}
(*sle)[epochField] = epoch + 1;
}
return tesSUCCESS;
};
if (auto const ter = setEncryptionKey(sfIssuerEncryptionKey, sfIssuerKeyEpoch);
!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
if (auto const ter = setEncryptionKey(sfAuditorEncryptionKey, sfAuditorKeyEpoch);
!isTesSuccess(ter))
return ter; // LCOV_EXCL_LINE
view().update(sle);

View File

@@ -0,0 +1,634 @@
#include <test/jtx/Account.h>
#include <test/jtx/ConfidentialTransfer.h>
#include <test/jtx/Env.h>
#include <test/jtx/mpt.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <cstdint>
#include <memory>
#include <optional>
namespace xrpl {
class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
{
void
testMPTokenIssuanceSetRotateIssuerKey(FeatureBitset features)
{
testcase("MPTokenIssuanceSet rotate issuer key");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
// First-time registration.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
});
// Verify that no epochs are set when registering for the first time.
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
// Rotating the issuer key requires the key rotation amendment
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(bob),
.err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
});
// A rotation replaces the issuer key and bumps its epoch. The auditor
// key was never registered, so it and its epoch stay absent.
if (rotationEnabled)
{
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, std::nullopt));
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
}
else
{
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
}
if (rotationEnabled)
{
// A second rotation increments the epoch again
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
});
BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, std::nullopt));
}
}
void
testMPTokenIssuanceSetRotateBothKeys(FeatureBitset features)
{
testcase("MPTokenIssuanceSet rotate both issuer and auditor keys");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const auditor("auditor");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(auditor);
// Register both keys together.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// Verify that no epochs are set when registering for the first time.
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
// Rotating both keys requires the amendment
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(bob),
.auditorPubKey = mptAlice.getPubKey(alice),
.err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
});
if (rotationEnabled)
{
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, alice));
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
}
else
{
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
}
if (rotationEnabled)
{
// Rotating the issuer key to its current value fails.
// Current issuer key is bob, duplicate.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(bob),
.err = tecDUPLICATE,
});
// Rotating the auditor key to its current value fails.
// Current auditor key is alice, duplicate.
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(alice),
.err = tecDUPLICATE,
});
// The whole transaction fails when one key is unchanged, even if
// the other key is rotated to a new value.
// Current issuer key is bob, duplicate.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(bob),
.auditorPubKey = mptAlice.getPubKey(auditor),
.err = tecDUPLICATE,
});
// Current auditor key is alice, duplicate.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(auditor),
.auditorPubKey = mptAlice.getPubKey(alice),
.err = tecDUPLICATE,
});
// Nothing changed: keys and epochs are untouched
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, 1u));
// A second rotation increments both epochs again
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, 2u));
}
}
void
testMPTokenIssuanceSetRotateAuditorKeyOnly(FeatureBitset features)
{
testcase("MPTokenIssuanceSet rotate auditor key only");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const auditor("auditor");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(auditor);
// Register both keys together.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// A transaction carrying only the auditor key fails preflight
// pre-ConfidentialMPTKeyRotation; post-ConfidentialMPTKeyRotation it rotates the auditor
// key
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(bob),
.err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
});
// The issuer key keeps unchanged, and rotating only the auditor key
// bumps only its epoch.
if (rotationEnabled)
{
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, bob));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
}
else
{
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
}
if (rotationEnabled)
{
// A second rotation increments the epoch again
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// The issuer key epoch is still untouched.
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 2u));
}
}
void
testMPTokenIssuanceSetRegisterAuditorKeyLater(FeatureBitset features)
{
testcase("MPTokenIssuanceSet register auditor key after issuer key");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const auditor("auditor");
MPTTester mptAlice(env, alice);
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(auditor);
// Register the issuer key first. We'll register the auditor key in a separate transaction.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
});
// Register the auditor key separately.
// pre-ConfidentialMPTKeyRotation it fails preflight; post-ConfidentialMPTKeyRotation it
// succeeds without touching any epoch because it's a first-time registration.
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(auditor),
.err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
});
BEAST_EXPECT(mptAlice.checkEncryptionKeys(
alice, rotationEnabled ? std::optional<Account>(auditor) : std::nullopt));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
}
void
testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(FeatureBitset features)
{
testcase("MPTokenIssuanceSet register auditor key later with circulating supply");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const auditor("auditor");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.authorize({.account = bob});
mptAlice.pay(alice, bob, 100);
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(auditor);
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
});
// Convert some of bob's balance so that COA > 0
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
auto const sleIssuanceBefore = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
if (!BEAST_EXPECT(sleIssuanceBefore))
return;
auto const coaBefore = (*sleIssuanceBefore)[~sfConfidentialOutstandingAmount].value_or(0);
BEAST_EXPECT(coaBefore > 0);
// Registering the auditor key for the first time while confidential
// supply is circulating: pre-ConfidentialMPTKeyRotation an auditor-only
// transaction fails preflight; post-ConfidentialMPTKeyRotation it
// succeeds as a first-time late-registration even COA > 0.
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(auditor),
.err = rotationEnabled ? TER(tesSUCCESS) : TER(temMALFORMED),
});
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
if (!BEAST_EXPECT(sleIssuance))
return;
BEAST_EXPECT(mptAlice.checkEncryptionKeys(
alice, rotationEnabled ? std::optional<Account>(auditor) : std::nullopt));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
// The circulating supply itself is not affected.
BEAST_EXPECT((*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBefore);
}
void
testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(FeatureBitset features)
{
testcase("MPTokenIssuanceSet auditor key requires issuer key");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const auditor("auditor");
MPTTester mptAlice(env, alice);
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.generateKeyPair(auditor);
// The issuer key was never registered. pre-ConfidentialMPTKeyRotation an auditor-only
// transaction fails preflight; post-ConfidentialMPTKeyRotation it passes preflight
// but preclaim rejects registering an auditor key on an issuance
// without an issuer key.
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(auditor),
.err = rotationEnabled ? TER(tecNO_PERMISSION) : TER(temMALFORMED),
});
// The rejected transaction leaves the issuance without either key.
BEAST_EXPECT(mptAlice.checkEncryptionKeys(std::nullopt, std::nullopt));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
}
void
testMPTokenIssuanceSetRotateWithCOA(FeatureBitset features)
{
testcase("MPTokenIssuanceSet rotate with circulating confidential supply");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.authorize({.account = bob});
mptAlice.pay(alice, bob, 100);
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(carol);
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
});
// Convert some of bob's balance to confidential spending, so that the
// issuance has confidential supply. COA > 0.
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
auto const sleIssuanceBeforeRotation =
env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
if (!BEAST_EXPECT(sleIssuanceBeforeRotation))
return;
auto const coaBeforeRotation =
(*sleIssuanceBeforeRotation)[~sfConfidentialOutstandingAmount].value_or(0);
BEAST_EXPECT(coaBeforeRotation > 0);
// Rotating key requires the
// amendment.
bool const rotationEnabled = features[featureConfidentialMPTKeyRotation];
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(carol),
.err = rotationEnabled ? TER(tesSUCCESS) : TER(tecNO_PERMISSION),
});
auto const sleIssuance = env.le(keylet::mptokenIssuance(mptAlice.issuanceID()));
if (!BEAST_EXPECT(sleIssuance))
return;
if (rotationEnabled)
{
BEAST_EXPECT(mptAlice.checkEncryptionKeys(carol, std::nullopt));
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
}
else
{
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, std::nullopt));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
}
// The confidential outstanding amount is not affected by the rotation
BEAST_EXPECT(
(*sleIssuance)[~sfConfidentialOutstandingAmount].value_or(0) == coaBeforeRotation);
// Re-enabling confidential balances while supply is circulating is
// rejected regardless of the ConfidentialMPTKeyRotation amendment.
mptAlice.set({
.account = alice,
.flags = tfMPTSetCanHoldConfidentialBalance,
.err = tecNO_PERMISSION,
});
}
void
testMPTokenIssuanceSetKeyEpochAtMax(FeatureBitset features)
{
using namespace test::jtx;
if (!features[featureConfidentialMPTKeyRotation])
return;
testcase("MPTokenIssuanceSet key epoch cannot wrap");
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
Account const auditor("auditor");
// Keep the ledger open so that we can write the key epochs directly into it.
MPTTester mptAlice(env, alice, {.holders = {bob}, .close = false});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance,
});
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(bob);
mptAlice.generateKeyPair(carol);
mptAlice.generateKeyPair(auditor);
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
auto const issuanceKeylet = keylet::mptokenIssuance(mptAlice.issuanceID());
// Writes the supplied key epochs straight into the open ledger so that
// the maximum epoch is reachable without submitting four billion
// rotations.
auto setEpochs = [&](std::optional<std::uint32_t> const& issuerKeyEpoch,
std::optional<std::uint32_t> const& auditorKeyEpoch) {
env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
auto const sle = view.read(issuanceKeylet);
if (!sle)
return false; // LCOV_EXCL_LINE
auto replacement = std::make_shared<SLE>(*sle);
if (issuerKeyEpoch)
(*replacement)[sfIssuerKeyEpoch] = *issuerKeyEpoch;
if (auditorKeyEpoch)
(*replacement)[sfAuditorKeyEpoch] = *auditorKeyEpoch;
view.rawReplace(replacement);
return true;
});
};
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
// Increment the auditor epoch to kMaxKeyEpoch - 1, leaving the issuer epoch absent.
setEpochs(std::nullopt, kMaxKeyEpoch - 1);
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch - 1));
// Rotating the auditor key to kMaxKeyEpoch succeeds.
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(carol),
});
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
// A further auditor rotation is rejected because the epoch is exhausted.
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(bob),
.err = tecNO_PERMISSION,
});
// Rotating both keys at once is rejected as a whole because the auditor
// epoch is exhausted.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(auditor),
.auditorPubKey = mptAlice.getPubKey(bob),
.err = tecNO_PERMISSION,
});
// Both rejections leave every key and epoch as it was.
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, carol));
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, kMaxKeyEpoch));
// The issuer key is unaffected by the exhausted auditor epoch.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(bob),
});
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, kMaxKeyEpoch));
// Increment the issuer epoch to kMaxKeyEpoch - 1.
setEpochs(kMaxKeyEpoch - 1, std::nullopt);
BEAST_EXPECT(mptAlice.checkEncryptionKeys(bob, carol));
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch - 1, kMaxKeyEpoch));
// Rotating the issuer key to kMaxKeyEpoch succeeds.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(auditor),
});
BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
// With both epochs exhausted neither key can be rotated again.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.err = tecNO_PERMISSION,
});
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(bob),
.err = tecNO_PERMISSION,
});
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(bob),
.err = tecNO_PERMISSION,
});
BEAST_EXPECT(mptAlice.checkEncryptionKeys(auditor, carol));
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
}
void
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
{
testMPTokenIssuanceSetRotateIssuerKey(features);
testMPTokenIssuanceSetRotateBothKeys(features);
testMPTokenIssuanceSetRotateAuditorKeyOnly(features);
testMPTokenIssuanceSetRegisterAuditorKeyLater(features);
testMPTokenIssuanceSetRegisterAuditorKeyLaterWithCOA(features);
testMPTokenIssuanceSetAuditorKeyWithoutIssuerKey(features);
testMPTokenIssuanceSetRotateWithCOA(features);
testMPTokenIssuanceSetKeyEpochAtMax(features);
}
public:
void
run() override
{
using namespace test::jtx;
FeatureBitset const all{testableAmendments()};
testMPTokenIssuanceSetWithFeats(all);
testMPTokenIssuanceSetWithFeats(all - featureConfidentialMPTKeyRotation);
}
};
BEAST_DEFINE_TESTSUITE(ConfidentialMPTKeyRotation, app, xrpl);
} // namespace xrpl

View File

@@ -736,12 +736,8 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
.err = temMALFORMED,
});
// Cannot set auditor key without issuer key
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(alice),
.err = temMALFORMED,
});
// Note: "auditor key without issuer key" (temMALFORMED before
// ConfidentialMPTKeyRotation) is covered in ConfidentialMPTKeyRotation_test
// Cannot set Holder and issuer Keys in the same transaction
mptAlice.set({
@@ -787,9 +783,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
});
}
// Cannot update issuer public key once set
// Cannot update issuer public key once set (pre-ConfidentialMPTKeyRotation behavior)
{
Env env{*this, features};
Env env{*this, features - featureConfidentialMPTKeyRotation};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
@@ -819,8 +815,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
// Cannot update issuer and auditor public keys once set
// Note: trying to set only auditor key fails in preflight (temMALFORMED)
// so we must provide both keys, which fails on issuer key check first
// (pre-ConfidentialMPTKeyRotation behavior)
{
Env env{*this, features};
Env env{*this, features - featureConfidentialMPTKeyRotation};
Account const alice("alice");
Account const bob("bob");
Account const auditor("auditor");
@@ -900,8 +897,9 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase
}
// Set issuer key first, then auditor key in a separate tx
// (pre-ConfidentialMPTKeyRotation behavior)
{
Env env{*this, features};
Env env{*this, features - featureConfidentialMPTKeyRotation};
Account const alice("alice");
Account const auditor("auditor");
MPTTester mptAlice(env, alice, {.holders = {}, .auditor = auditor});

View File

@@ -648,6 +648,38 @@ MPTTester::checkImmutableFlags(std::uint32_t expectedFlags) const
});
}
[[nodiscard]] bool
MPTTester::checkKeyEpochs(
std::optional<std::uint32_t> issuerKeyEpoch,
std::optional<std::uint32_t> auditorKeyEpoch) const
{
return forObject([&](SLEP const& sle) -> bool {
return (*sle)[~sfIssuerKeyEpoch] == issuerKeyEpoch &&
(*sle)[~sfAuditorKeyEpoch] == auditorKeyEpoch;
});
}
[[nodiscard]] bool
MPTTester::checkEncryptionKeys(
std::optional<Account> const& issuerKeyOwner,
std::optional<Account> const& auditorKeyOwner) const
{
auto const matches =
[this](SLEP const& sle, SF_VL const& field, std::optional<Account> const& owner) {
if (!owner)
return !sle->isFieldPresent(field);
auto const expected = getPubKey(*owner);
return expected && sle->isFieldPresent(field) &&
strHex((*sle)[field]) == strHex(*expected);
};
return forObject([&](SLEP const& sle) -> bool {
return matches(sle, sfIssuerEncryptionKey, issuerKeyOwner) &&
matches(sle, sfAuditorEncryptionKey, auditorKeyOwner);
});
}
void
MPTTester::pay(
Account const& src,

View File

@@ -612,6 +612,21 @@ public:
[[nodiscard]] bool
checkImmutableFlags(std::uint32_t expectedFlags) const;
// Checks both key epochs on the issuance. Pass std::nullopt for an epoch
// that is expected to be absent, which means the key is never rotated.
[[nodiscard]] bool
checkKeyEpochs(
std::optional<std::uint32_t> issuerKeyEpoch,
std::optional<std::uint32_t> auditorKeyEpoch) const;
// Checks that the issuance carries the encryption keys of the given
// accounts. Pass std::nullopt for a key that is expected to be absent,
// which means the key is never registered.
[[nodiscard]] bool
checkEncryptionKeys(
std::optional<Account> const& issuerKeyOwner,
std::optional<Account> const& auditorKeyOwner) const;
[[nodiscard]] Account const&
issuer() const
{

View File

@@ -27,6 +27,8 @@ TEST(EscrowTests, BuilderSettersRoundTrip)
auto const conditionValue = canonical_VL();
auto const cancelAfterValue = canonical_UINT32();
auto const finishAfterValue = canonical_UINT32();
auto const bytecodeValue = canonical_VL();
auto const dataValue = canonical_VL();
auto const sourceTagValue = canonical_UINT32();
auto const destinationTagValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
@@ -49,6 +51,8 @@ TEST(EscrowTests, BuilderSettersRoundTrip)
builder.setCondition(conditionValue);
builder.setCancelAfter(cancelAfterValue);
builder.setFinishAfter(finishAfterValue);
builder.setBytecode(bytecodeValue);
builder.setData(dataValue);
builder.setSourceTag(sourceTagValue);
builder.setDestinationTag(destinationTagValue);
builder.setDestinationNode(destinationNodeValue);
@@ -132,6 +136,22 @@ TEST(EscrowTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasFinishAfter());
}
{
auto const& expected = bytecodeValue;
auto const actualOpt = entry.getBytecode();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfBytecode");
EXPECT_TRUE(entry.hasBytecode());
}
{
auto const& expected = dataValue;
auto const actualOpt = entry.getData();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfData");
EXPECT_TRUE(entry.hasData());
}
{
auto const& expected = sourceTagValue;
auto const actualOpt = entry.getSourceTag();
@@ -192,6 +212,8 @@ TEST(EscrowTests, BuilderFromSleRoundTrip)
auto const conditionValue = canonical_VL();
auto const cancelAfterValue = canonical_UINT32();
auto const finishAfterValue = canonical_UINT32();
auto const bytecodeValue = canonical_VL();
auto const dataValue = canonical_VL();
auto const sourceTagValue = canonical_UINT32();
auto const destinationTagValue = canonical_UINT32();
auto const ownerNodeValue = canonical_UINT64();
@@ -210,6 +232,8 @@ TEST(EscrowTests, BuilderFromSleRoundTrip)
sle->at(sfCondition) = conditionValue;
sle->at(sfCancelAfter) = cancelAfterValue;
sle->at(sfFinishAfter) = finishAfterValue;
sle->at(sfBytecode) = bytecodeValue;
sle->at(sfData) = dataValue;
sle->at(sfSourceTag) = sourceTagValue;
sle->at(sfDestinationTag) = destinationTagValue;
sle->at(sfOwnerNode) = ownerNodeValue;
@@ -340,6 +364,32 @@ TEST(EscrowTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfFinishAfter");
}
{
auto const& expected = bytecodeValue;
auto const fromSleOpt = entryFromSle.getBytecode();
auto const fromBuilderOpt = entryFromBuilder.getBytecode();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfBytecode");
expectEqualField(expected, *fromBuilderOpt, "sfBytecode");
}
{
auto const& expected = dataValue;
auto const fromSleOpt = entryFromSle.getData();
auto const fromBuilderOpt = entryFromBuilder.getData();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfData");
expectEqualField(expected, *fromBuilderOpt, "sfData");
}
{
auto const& expected = sourceTagValue;
@@ -477,6 +527,10 @@ TEST(EscrowTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getCancelAfter().has_value());
EXPECT_FALSE(entry.hasFinishAfter());
EXPECT_FALSE(entry.getFinishAfter().has_value());
EXPECT_FALSE(entry.hasBytecode());
EXPECT_FALSE(entry.getBytecode().has_value());
EXPECT_FALSE(entry.hasData());
EXPECT_FALSE(entry.getData().has_value());
EXPECT_FALSE(entry.hasSourceTag());
EXPECT_FALSE(entry.getSourceTag().has_value());
EXPECT_FALSE(entry.hasDestinationTag());

View File

@@ -27,6 +27,9 @@ TEST(FeeSettingsTests, BuilderSettersRoundTrip)
auto const baseFeeDropsValue = canonical_AMOUNT();
auto const reserveBaseDropsValue = canonical_AMOUNT();
auto const reserveIncrementDropsValue = canonical_AMOUNT();
auto const gasLimitValue = canonical_UINT32();
auto const bytecodeSizeLimitValue = canonical_UINT32();
auto const gasPriceValue = canonical_UINT32();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
@@ -40,6 +43,9 @@ TEST(FeeSettingsTests, BuilderSettersRoundTrip)
builder.setBaseFeeDrops(baseFeeDropsValue);
builder.setReserveBaseDrops(reserveBaseDropsValue);
builder.setReserveIncrementDrops(reserveIncrementDropsValue);
builder.setGasLimit(gasLimitValue);
builder.setBytecodeSizeLimit(bytecodeSizeLimitValue);
builder.setGasPrice(gasPriceValue);
builder.setPreviousTxnID(previousTxnIDValue);
builder.setPreviousTxnLgrSeq(previousTxnLgrSeqValue);
@@ -108,6 +114,30 @@ TEST(FeeSettingsTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasReserveIncrementDrops());
}
{
auto const& expected = gasLimitValue;
auto const actualOpt = entry.getGasLimit();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfGasLimit");
EXPECT_TRUE(entry.hasGasLimit());
}
{
auto const& expected = bytecodeSizeLimitValue;
auto const actualOpt = entry.getBytecodeSizeLimit();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfBytecodeSizeLimit");
EXPECT_TRUE(entry.hasBytecodeSizeLimit());
}
{
auto const& expected = gasPriceValue;
auto const actualOpt = entry.getGasPrice();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfGasPrice");
EXPECT_TRUE(entry.hasGasPrice());
}
{
auto const& expected = previousTxnIDValue;
auto const actualOpt = entry.getPreviousTxnID();
@@ -144,6 +174,9 @@ TEST(FeeSettingsTests, BuilderFromSleRoundTrip)
auto const baseFeeDropsValue = canonical_AMOUNT();
auto const reserveBaseDropsValue = canonical_AMOUNT();
auto const reserveIncrementDropsValue = canonical_AMOUNT();
auto const gasLimitValue = canonical_UINT32();
auto const bytecodeSizeLimitValue = canonical_UINT32();
auto const gasPriceValue = canonical_UINT32();
auto const previousTxnIDValue = canonical_UINT256();
auto const previousTxnLgrSeqValue = canonical_UINT32();
@@ -156,6 +189,9 @@ TEST(FeeSettingsTests, BuilderFromSleRoundTrip)
sle->at(sfBaseFeeDrops) = baseFeeDropsValue;
sle->at(sfReserveBaseDrops) = reserveBaseDropsValue;
sle->at(sfReserveIncrementDrops) = reserveIncrementDropsValue;
sle->at(sfGasLimit) = gasLimitValue;
sle->at(sfBytecodeSizeLimit) = bytecodeSizeLimitValue;
sle->at(sfGasPrice) = gasPriceValue;
sle->at(sfPreviousTxnID) = previousTxnIDValue;
sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue;
@@ -259,6 +295,45 @@ TEST(FeeSettingsTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfReserveIncrementDrops");
}
{
auto const& expected = gasLimitValue;
auto const fromSleOpt = entryFromSle.getGasLimit();
auto const fromBuilderOpt = entryFromBuilder.getGasLimit();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfGasLimit");
expectEqualField(expected, *fromBuilderOpt, "sfGasLimit");
}
{
auto const& expected = bytecodeSizeLimitValue;
auto const fromSleOpt = entryFromSle.getBytecodeSizeLimit();
auto const fromBuilderOpt = entryFromBuilder.getBytecodeSizeLimit();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfBytecodeSizeLimit");
expectEqualField(expected, *fromBuilderOpt, "sfBytecodeSizeLimit");
}
{
auto const& expected = gasPriceValue;
auto const fromSleOpt = entryFromSle.getGasPrice();
auto const fromBuilderOpt = entryFromBuilder.getGasPrice();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfGasPrice");
expectEqualField(expected, *fromBuilderOpt, "sfGasPrice");
}
{
auto const& expected = previousTxnIDValue;
@@ -351,6 +426,12 @@ TEST(FeeSettingsTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getReserveBaseDrops().has_value());
EXPECT_FALSE(entry.hasReserveIncrementDrops());
EXPECT_FALSE(entry.getReserveIncrementDrops().has_value());
EXPECT_FALSE(entry.hasGasLimit());
EXPECT_FALSE(entry.getGasLimit().has_value());
EXPECT_FALSE(entry.hasBytecodeSizeLimit());
EXPECT_FALSE(entry.getBytecodeSizeLimit().has_value());
EXPECT_FALSE(entry.hasGasPrice());
EXPECT_FALSE(entry.getGasPrice().has_value());
EXPECT_FALSE(entry.hasPreviousTxnID());
EXPECT_FALSE(entry.getPreviousTxnID().has_value());
EXPECT_FALSE(entry.hasPreviousTxnLgrSeq());

View File

@@ -36,6 +36,8 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
auto const referenceHoldingValue = canonical_UINT256();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
auto const issuerKeyEpochValue = canonical_UINT32();
auto const auditorKeyEpochValue = canonical_UINT32();
auto const confidentialOutstandingAmountValue = canonical_UINT64();
MPTokenIssuanceBuilder builder{
@@ -57,6 +59,8 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
builder.setReferenceHolding(referenceHoldingValue);
builder.setIssuerEncryptionKey(issuerEncryptionKeyValue);
builder.setAuditorEncryptionKey(auditorEncryptionKeyValue);
builder.setIssuerKeyEpoch(issuerKeyEpochValue);
builder.setAuditorKeyEpoch(auditorKeyEpochValue);
builder.setConfidentialOutstandingAmount(confidentialOutstandingAmountValue);
builder.setLedgerIndex(index);
@@ -184,6 +188,22 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasAuditorEncryptionKey());
}
{
auto const& expected = issuerKeyEpochValue;
auto const actualOpt = entry.getIssuerKeyEpoch();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfIssuerKeyEpoch");
EXPECT_TRUE(entry.hasIssuerKeyEpoch());
}
{
auto const& expected = auditorKeyEpochValue;
auto const actualOpt = entry.getAuditorKeyEpoch();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfAuditorKeyEpoch");
EXPECT_TRUE(entry.hasAuditorKeyEpoch());
}
{
auto const& expected = confidentialOutstandingAmountValue;
auto const actualOpt = entry.getConfidentialOutstandingAmount();
@@ -221,6 +241,8 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
auto const referenceHoldingValue = canonical_UINT256();
auto const issuerEncryptionKeyValue = canonical_VL();
auto const auditorEncryptionKeyValue = canonical_VL();
auto const issuerKeyEpochValue = canonical_UINT32();
auto const auditorKeyEpochValue = canonical_UINT32();
auto const confidentialOutstandingAmountValue = canonical_UINT64();
auto sle = std::make_shared<SLE>(MPTokenIssuance::entryType, index);
@@ -241,6 +263,8 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
sle->at(sfReferenceHolding) = referenceHoldingValue;
sle->at(sfIssuerEncryptionKey) = issuerEncryptionKeyValue;
sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue;
sle->at(sfIssuerKeyEpoch) = issuerKeyEpochValue;
sle->at(sfAuditorKeyEpoch) = auditorKeyEpochValue;
sle->at(sfConfidentialOutstandingAmount) = confidentialOutstandingAmountValue;
MPTokenIssuanceBuilder builderFromSle{sle};
@@ -442,6 +466,32 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptionKey");
}
{
auto const& expected = issuerKeyEpochValue;
auto const fromSleOpt = entryFromSle.getIssuerKeyEpoch();
auto const fromBuilderOpt = entryFromBuilder.getIssuerKeyEpoch();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfIssuerKeyEpoch");
expectEqualField(expected, *fromBuilderOpt, "sfIssuerKeyEpoch");
}
{
auto const& expected = auditorKeyEpochValue;
auto const fromSleOpt = entryFromSle.getAuditorKeyEpoch();
auto const fromBuilderOpt = entryFromBuilder.getAuditorKeyEpoch();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfAuditorKeyEpoch");
expectEqualField(expected, *fromBuilderOpt, "sfAuditorKeyEpoch");
}
{
auto const& expected = confidentialOutstandingAmountValue;
@@ -539,6 +589,10 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getIssuerEncryptionKey().has_value());
EXPECT_FALSE(entry.hasAuditorEncryptionKey());
EXPECT_FALSE(entry.getAuditorEncryptionKey().has_value());
EXPECT_FALSE(entry.hasIssuerKeyEpoch());
EXPECT_FALSE(entry.getIssuerKeyEpoch().has_value());
EXPECT_FALSE(entry.hasAuditorKeyEpoch());
EXPECT_FALSE(entry.getAuditorKeyEpoch().has_value());
EXPECT_FALSE(entry.hasConfidentialOutstandingAmount());
EXPECT_FALSE(entry.getConfidentialOutstandingAmount().has_value());
}

View File

@@ -30,11 +30,13 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip)
// Transaction-specific field values
auto const destinationValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const amountValue = canonical_AMOUNT();
auto const conditionValue = canonical_VL();
auto const cancelAfterValue = canonical_UINT32();
auto const finishAfterValue = canonical_UINT32();
auto const destinationTagValue = canonical_UINT32();
auto const bytecodeValue = canonical_VL();
auto const dataValue = canonical_VL();
EscrowCreateBuilder builder{
accountValue,
@@ -45,10 +47,12 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip)
};
// Set optional fields
builder.setDestinationTag(destinationTagValue);
builder.setCondition(conditionValue);
builder.setCancelAfter(cancelAfterValue);
builder.setFinishAfter(finishAfterValue);
builder.setDestinationTag(destinationTagValue);
builder.setBytecode(bytecodeValue);
builder.setData(dataValue);
auto tx = builder.build(publicKey, secretKey);
@@ -78,6 +82,14 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip)
}
// Verify optional fields
{
auto const& expected = destinationTagValue;
auto const actualOpt = tx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(tx.hasDestinationTag());
}
{
auto const& expected = conditionValue;
auto const actualOpt = tx.getCondition();
@@ -103,11 +115,19 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip)
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = tx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
EXPECT_TRUE(tx.hasDestinationTag());
auto const& expected = bytecodeValue;
auto const actualOpt = tx.getBytecode();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecode should be present";
expectEqualField(expected, *actualOpt, "sfBytecode");
EXPECT_TRUE(tx.hasBytecode());
}
{
auto const& expected = dataValue;
auto const actualOpt = tx.getData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfData should be present";
expectEqualField(expected, *actualOpt, "sfData");
EXPECT_TRUE(tx.hasData());
}
}
@@ -127,11 +147,13 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip)
// Transaction-specific field values
auto const destinationValue = canonical_ACCOUNT();
auto const destinationTagValue = canonical_UINT32();
auto const amountValue = canonical_AMOUNT();
auto const conditionValue = canonical_VL();
auto const cancelAfterValue = canonical_UINT32();
auto const finishAfterValue = canonical_UINT32();
auto const destinationTagValue = canonical_UINT32();
auto const bytecodeValue = canonical_VL();
auto const dataValue = canonical_VL();
// Build an initial transaction
EscrowCreateBuilder initialBuilder{
@@ -142,10 +164,12 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip)
feeValue
};
initialBuilder.setDestinationTag(destinationTagValue);
initialBuilder.setCondition(conditionValue);
initialBuilder.setCancelAfter(cancelAfterValue);
initialBuilder.setFinishAfter(finishAfterValue);
initialBuilder.setDestinationTag(destinationTagValue);
initialBuilder.setBytecode(bytecodeValue);
initialBuilder.setData(dataValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -176,6 +200,13 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip)
}
// Verify optional fields
{
auto const& expected = destinationTagValue;
auto const actualOpt = rebuiltTx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
}
{
auto const& expected = conditionValue;
auto const actualOpt = rebuiltTx.getCondition();
@@ -198,10 +229,17 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip)
}
{
auto const& expected = destinationTagValue;
auto const actualOpt = rebuiltTx.getDestinationTag();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present";
expectEqualField(expected, *actualOpt, "sfDestinationTag");
auto const& expected = bytecodeValue;
auto const actualOpt = rebuiltTx.getBytecode();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecode should be present";
expectEqualField(expected, *actualOpt, "sfBytecode");
}
{
auto const& expected = dataValue;
auto const actualOpt = rebuiltTx.getData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfData should be present";
expectEqualField(expected, *actualOpt, "sfData");
}
}
@@ -263,14 +301,18 @@ TEST(TransactionsEscrowCreateTests, OptionalFieldsReturnNullopt)
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasDestinationTag());
EXPECT_FALSE(tx.getDestinationTag().has_value());
EXPECT_FALSE(tx.hasCondition());
EXPECT_FALSE(tx.getCondition().has_value());
EXPECT_FALSE(tx.hasCancelAfter());
EXPECT_FALSE(tx.getCancelAfter().has_value());
EXPECT_FALSE(tx.hasFinishAfter());
EXPECT_FALSE(tx.getFinishAfter().has_value());
EXPECT_FALSE(tx.hasDestinationTag());
EXPECT_FALSE(tx.getDestinationTag().has_value());
EXPECT_FALSE(tx.hasBytecode());
EXPECT_FALSE(tx.getBytecode().has_value());
EXPECT_FALSE(tx.hasData());
EXPECT_FALSE(tx.getData().has_value());
}
}

View File

@@ -34,6 +34,7 @@ TEST(TransactionsEscrowFinishTests, BuilderSettersRoundTrip)
auto const fulfillmentValue = canonical_VL();
auto const conditionValue = canonical_VL();
auto const credentialIDsValue = canonical_VECTOR256();
auto const gasValue = canonical_UINT32();
EscrowFinishBuilder builder{
accountValue,
@@ -47,6 +48,7 @@ TEST(TransactionsEscrowFinishTests, BuilderSettersRoundTrip)
builder.setFulfillment(fulfillmentValue);
builder.setCondition(conditionValue);
builder.setCredentialIDs(credentialIDsValue);
builder.setGas(gasValue);
auto tx = builder.build(publicKey, secretKey);
@@ -100,6 +102,14 @@ TEST(TransactionsEscrowFinishTests, BuilderSettersRoundTrip)
EXPECT_TRUE(tx.hasCredentialIDs());
}
{
auto const& expected = gasValue;
auto const actualOpt = tx.getGas();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGas should be present";
expectEqualField(expected, *actualOpt, "sfGas");
EXPECT_TRUE(tx.hasGas());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -121,6 +131,7 @@ TEST(TransactionsEscrowFinishTests, BuilderFromStTxRoundTrip)
auto const fulfillmentValue = canonical_VL();
auto const conditionValue = canonical_VL();
auto const credentialIDsValue = canonical_VECTOR256();
auto const gasValue = canonical_UINT32();
// Build an initial transaction
EscrowFinishBuilder initialBuilder{
@@ -134,6 +145,7 @@ TEST(TransactionsEscrowFinishTests, BuilderFromStTxRoundTrip)
initialBuilder.setFulfillment(fulfillmentValue);
initialBuilder.setCondition(conditionValue);
initialBuilder.setCredentialIDs(credentialIDsValue);
initialBuilder.setGas(gasValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -185,6 +197,13 @@ TEST(TransactionsEscrowFinishTests, BuilderFromStTxRoundTrip)
expectEqualField(expected, *actualOpt, "sfCredentialIDs");
}
{
auto const& expected = gasValue;
auto const actualOpt = rebuiltTx.getGas();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGas should be present";
expectEqualField(expected, *actualOpt, "sfGas");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -250,6 +269,8 @@ TEST(TransactionsEscrowFinishTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(tx.getCondition().has_value());
EXPECT_FALSE(tx.hasCredentialIDs());
EXPECT_FALSE(tx.getCredentialIDs().has_value());
EXPECT_FALSE(tx.hasGas());
EXPECT_FALSE(tx.getGas().has_value());
}
}

View File

@@ -37,6 +37,9 @@ TEST(TransactionsSetFeeTests, BuilderSettersRoundTrip)
auto const baseFeeDropsValue = canonical_AMOUNT();
auto const reserveBaseDropsValue = canonical_AMOUNT();
auto const reserveIncrementDropsValue = canonical_AMOUNT();
auto const gasLimitValue = canonical_UINT32();
auto const bytecodeSizeLimitValue = canonical_UINT32();
auto const gasPriceValue = canonical_UINT32();
SetFeeBuilder builder{
accountValue,
@@ -53,6 +56,9 @@ TEST(TransactionsSetFeeTests, BuilderSettersRoundTrip)
builder.setBaseFeeDrops(baseFeeDropsValue);
builder.setReserveBaseDrops(reserveBaseDropsValue);
builder.setReserveIncrementDrops(reserveIncrementDropsValue);
builder.setGasLimit(gasLimitValue);
builder.setBytecodeSizeLimit(bytecodeSizeLimitValue);
builder.setGasPrice(gasPriceValue);
auto tx = builder.build(publicKey, secretKey);
@@ -134,6 +140,30 @@ TEST(TransactionsSetFeeTests, BuilderSettersRoundTrip)
EXPECT_TRUE(tx.hasReserveIncrementDrops());
}
{
auto const& expected = gasLimitValue;
auto const actualOpt = tx.getGasLimit();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasLimit should be present";
expectEqualField(expected, *actualOpt, "sfGasLimit");
EXPECT_TRUE(tx.hasGasLimit());
}
{
auto const& expected = bytecodeSizeLimitValue;
auto const actualOpt = tx.getBytecodeSizeLimit();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecodeSizeLimit should be present";
expectEqualField(expected, *actualOpt, "sfBytecodeSizeLimit");
EXPECT_TRUE(tx.hasBytecodeSizeLimit());
}
{
auto const& expected = gasPriceValue;
auto const actualOpt = tx.getGasPrice();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasPrice should be present";
expectEqualField(expected, *actualOpt, "sfGasPrice");
EXPECT_TRUE(tx.hasGasPrice());
}
}
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -158,6 +188,9 @@ TEST(TransactionsSetFeeTests, BuilderFromStTxRoundTrip)
auto const baseFeeDropsValue = canonical_AMOUNT();
auto const reserveBaseDropsValue = canonical_AMOUNT();
auto const reserveIncrementDropsValue = canonical_AMOUNT();
auto const gasLimitValue = canonical_UINT32();
auto const bytecodeSizeLimitValue = canonical_UINT32();
auto const gasPriceValue = canonical_UINT32();
// Build an initial transaction
SetFeeBuilder initialBuilder{
@@ -174,6 +207,9 @@ TEST(TransactionsSetFeeTests, BuilderFromStTxRoundTrip)
initialBuilder.setBaseFeeDrops(baseFeeDropsValue);
initialBuilder.setReserveBaseDrops(reserveBaseDropsValue);
initialBuilder.setReserveIncrementDrops(reserveIncrementDropsValue);
initialBuilder.setGasLimit(gasLimitValue);
initialBuilder.setBytecodeSizeLimit(bytecodeSizeLimitValue);
initialBuilder.setGasPrice(gasPriceValue);
auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -248,6 +284,27 @@ TEST(TransactionsSetFeeTests, BuilderFromStTxRoundTrip)
expectEqualField(expected, *actualOpt, "sfReserveIncrementDrops");
}
{
auto const& expected = gasLimitValue;
auto const actualOpt = rebuiltTx.getGasLimit();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasLimit should be present";
expectEqualField(expected, *actualOpt, "sfGasLimit");
}
{
auto const& expected = bytecodeSizeLimitValue;
auto const actualOpt = rebuiltTx.getBytecodeSizeLimit();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecodeSizeLimit should be present";
expectEqualField(expected, *actualOpt, "sfBytecodeSizeLimit");
}
{
auto const& expected = gasPriceValue;
auto const actualOpt = rebuiltTx.getGasPrice();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasPrice should be present";
expectEqualField(expected, *actualOpt, "sfGasPrice");
}
}
// 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -319,6 +376,12 @@ TEST(TransactionsSetFeeTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(tx.getReserveBaseDrops().has_value());
EXPECT_FALSE(tx.hasReserveIncrementDrops());
EXPECT_FALSE(tx.getReserveIncrementDrops().has_value());
EXPECT_FALSE(tx.hasGasLimit());
EXPECT_FALSE(tx.getGasLimit().has_value());
EXPECT_FALSE(tx.hasBytecodeSizeLimit());
EXPECT_FALSE(tx.getBytecodeSizeLimit().has_value());
EXPECT_FALSE(tx.hasGasPrice());
EXPECT_FALSE(tx.getGasPrice().has_value());
}
}

View File

@@ -19,6 +19,7 @@
#include <xrpld/app/main/LoadManager.h>
#include <xrpld/app/main/NodeIdentity.h>
#include <xrpld/app/main/NodeStoreScheduler.h>
#include <xrpld/app/misc/DatagramMonitor.h>
#include <xrpld/app/misc/SHAMapStore.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpld/app/misc/ValidatorKeys.h>
@@ -222,6 +223,7 @@ public:
std::unique_ptr<JobQueue> jobQueue_;
NodeStoreScheduler nodeStoreScheduler_;
std::unique_ptr<SHAMapStore> shaMapStore_;
std::unique_ptr<DatagramMonitor> datagramMonitor_;
PendingSaves pendingSaves_;
std::optional<OpenLedger> openLedger_;
@@ -1525,6 +1527,14 @@ ApplicationImp::start(bool withTimers)
ledgerCleaner_->start();
perfLog_->start();
// Datagram monitor: UDP node-stats exporter (XDGM). Off in standalone or
// when [datagram_monitor] has no endpoints.
if (!config_->standalone() && !config_->DATAGRAM_MONITOR.empty())
{
datagramMonitor_ = std::make_unique<DatagramMonitor>(*this);
datagramMonitor_->start();
}
}
void

View File

@@ -0,0 +1,880 @@
#pragma once
#include <xrpld/app/ledger/AcceptedLedger.h>
#include <xrpld/app/ledger/InboundLedgers.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/ValidatorList.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/overlay/Overlay.h>
#include <xrpl/basics/UptimeClock.h>
#include <xrpl/basics/mulDiv.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/CachedSLEs.h>
#include <xrpl/nodestore/Database.h>
#include <xrpl/protocol/BuildInfo.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/LoadFeeTrack.h>
#include <xrpl/server/NetworkOPs.h>
#include <arpa/inet.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <netdb.h>
#include <array>
#include <atomic>
#include <chrono>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#if defined(__linux__)
#include <sys/statvfs.h>
#include <sys/sysinfo.h>
#elif defined(__APPLE__)
#include <mach/host_info.h>
#include <mach/mach.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <sys/mount.h>
#include <sys/sysctl.h>
#include <sys/types.h>
#include <ifaddrs.h>
#endif
#include <thread>
#include <vector>
namespace xrpl {
// Magic number for server info packets: 'XDGM' (le) Xahau DataGram Monitor
constexpr uint32_t SERVER_INFO_MAGIC = 0x4D474458;
constexpr uint32_t SERVER_INFO_VERSION = 1;
// Warning flag bits
constexpr uint32_t WARNING_AMENDMENT_BLOCKED = 1 << 0;
constexpr uint32_t WARNING_UNL_BLOCKED = 1 << 1;
constexpr uint32_t WARNING_AMENDMENT_WARNED = 1 << 2;
constexpr uint32_t WARNING_NOT_SYNCED = 1 << 3;
// Time window statistics for rates
struct [[gnu::packed]] MetricRates
{
double rate_1m; // Average rate over last minute
double rate_5m; // Average rate over last 5 minutes
double rate_1h; // Average rate over last hour
double rate_24h; // Average rate over last 24 hours
};
struct AllRates
{
MetricRates network_in;
MetricRates network_out;
MetricRates disk_read;
MetricRates disk_write;
};
// Structure to represent a ledger sequence range
struct [[gnu::packed]] LgrRange
{
uint32_t start;
uint32_t end;
};
// Map is returned separately since variable-length data
// shouldn't be included in network structures
using ObjectCountMap = std::vector<std::pair<std::basic_string<char>, int>>;
struct [[gnu::packed]] DebugCounters
{
// Database metrics
std::uint64_t dbKBTotal{0};
std::uint64_t dbKBLedger{0};
std::uint64_t dbKBTransaction{0};
std::uint64_t localTxCount{0};
// Basic metrics
std::uint32_t writeLoad{0};
std::int32_t historicalPerMinute{0};
// Cache metrics
std::uint32_t sleHitRate{0}; // Stored as fixed point, multiplied by 1000
std::uint32_t ledgerHitRate{0}; // Stored as fixed point, multiplied by 1000
std::uint32_t alSize{0};
std::uint32_t alHitRate{0}; // Stored as fixed point, multiplied by 1000
std::int32_t fullbelowSize{0};
std::uint32_t treenodeCacheSize{0};
std::uint32_t treenodeTrackSize{0};
// Node store metrics
std::uint64_t nodeWriteCount{0};
std::uint64_t nodeWriteSize{0};
std::uint64_t nodeFetchCount{0};
std::uint64_t nodeFetchHitCount{0};
std::uint64_t nodeFetchSize{0};
};
// Core server metrics in the fixed header
struct [[gnu::packed]] ServerInfoHeader
{
// Fixed header fields come first
uint32_t magic; // Magic number to identify packet type
uint32_t version; // Protocol version number
uint32_t network_id; // Network ID from config
uint32_t server_state; // Operating mode as enum
uint32_t peer_count; // Number of connected peers
uint32_t node_size; // Size category (0=tiny through 4=huge)
uint32_t cpu_cores; // CPU core count
uint32_t ledger_range_count; // Number of range entries
uint32_t warning_flags; // Warning flags (reduced size)
uint32_t padding_1; // padding for alignment
// 64-bit metrics
uint64_t timestamp; // System time in microseconds
uint64_t uptime; // Server uptime in seconds
uint64_t io_latency_us; // IO latency in microseconds
uint64_t validation_quorum; // Validation quorum count
uint64_t fetch_pack_size; // Size of fetch pack cache
uint64_t proposer_count; // Number of proposers in last close
uint64_t converge_time_ms; // Last convergence time in ms
uint64_t load_factor; // Load factor (scaled by 1M)
uint64_t load_base; // Load base value
uint64_t reserve_base; // Reserve base amount
uint64_t reserve_inc; // Reserve increment amount
uint64_t ledger_seq; // Latest ledger sequence
// Fixed-size byte arrays
uint8_t ledger_hash[32]; // Latest ledger hash
uint8_t node_public_key[33]; // Node's public key
uint8_t padding2[7]; // Padding to maintain 8-byte alignment
uint8_t version_string[32];
// System metrics
uint64_t process_memory_pages; // Process memory usage in bytes
uint64_t system_memory_total; // Total system memory in bytes
uint64_t system_memory_free; // Free system memory in bytes
uint64_t system_memory_used; // Used system memory in bytes
uint64_t system_disk_total; // Total disk space in bytes
uint64_t system_disk_free; // Free disk space in bytes
uint64_t system_disk_used; // Used disk space in bytes
uint64_t io_wait_time; // IO wait time in milliseconds
double load_avg_1min; // 1 minute load average
double load_avg_5min; // 5 minute load average
double load_avg_15min; // 15 minute load average
// State transition metrics
uint64_t state_transitions[5]; // Count for each operating mode
uint64_t state_durations[5]; // Duration in each mode
uint64_t initial_sync_us; // Initial sync duration
// Network and disk rates remain unchanged
struct
{
MetricRates network_in;
MetricRates network_out;
MetricRates disk_read;
MetricRates disk_write;
} rates;
DebugCounters dbg_counters;
};
// System metrics collected for rate calculations
struct SystemMetrics
{
uint64_t timestamp; // When metrics were collected
uint64_t network_bytes_in; // Current total bytes in
uint64_t network_bytes_out; // Current total bytes out
uint64_t disk_bytes_read; // Current total bytes read
uint64_t disk_bytes_written; // Current total bytes written
};
class MetricsTracker
{
private:
static constexpr size_t SAMPLES_1M = 60; // 1 sample/second for 1 minute
static constexpr size_t SAMPLES_5M = 300; // 1 sample/second for 5 minutes
static constexpr size_t SAMPLES_1H = 3600; // 1 sample/second for 1 hour
static constexpr size_t SAMPLES_24H = 1440; // 1 sample/minute for 24 hours
std::vector<SystemMetrics> samples_1m{SAMPLES_1M};
std::vector<SystemMetrics> samples_5m{SAMPLES_5M};
std::vector<SystemMetrics> samples_1h{SAMPLES_1H};
std::vector<SystemMetrics> samples_24h{SAMPLES_24H};
size_t index_1m{0}, index_5m{0}, index_1h{0}, index_24h{0};
std::chrono::system_clock::time_point last_24h_sample{};
double
calculateRate(
SystemMetrics const& current,
std::vector<SystemMetrics> const& samples,
size_t current_index,
size_t max_samples,
bool is_24h_window,
std::function<uint64_t(SystemMetrics const&)> metric_getter)
{
// If we don't have at least 2 samples, the rate is 0
if (current_index < 2)
{
return 0.0;
}
// Calculate time window based on the window type
uint64_t expected_window_micros;
if (is_24h_window)
{
expected_window_micros =
24ULL * 60ULL * 60ULL * 1000000ULL; // 24 hours in microseconds
}
else
{
expected_window_micros =
max_samples * 1000000ULL; // window in seconds * 1,000,000 for microseconds
}
// For any window where we don't have full data, we should scale the
// rate based on the actual time we have data for
uint64_t actual_window_micros = current.timestamp - samples[0].timestamp;
double window_scale =
std::min(1.0, static_cast<double>(actual_window_micros) / expected_window_micros);
// Get the oldest valid sample
size_t oldest_index =
(current_index >= max_samples) ? ((current_index + 1) % max_samples) : 0;
auto const& oldest = samples[oldest_index];
double elapsed = actual_window_micros / 1000000.0; // Convert microseconds to seconds
// Ensure we have a meaningful time difference
if (elapsed < 0.001)
{ // Less than 1ms difference
return 0.0;
}
uint64_t current_value = metric_getter(current);
uint64_t oldest_value = metric_getter(oldest);
// Handle counter wraparound
uint64_t diff = (current_value >= oldest_value)
? (current_value - oldest_value)
: (std::numeric_limits<uint64_t>::max() - oldest_value + current_value + 1);
// Calculate the rate and scale it based on our window coverage
return (static_cast<double>(diff) / elapsed) * window_scale;
}
MetricRates
calculateMetricRates(
SystemMetrics const& current,
std::function<uint64_t(SystemMetrics const&)> metric_getter)
{
MetricRates rates;
rates.rate_1m =
calculateRate(current, samples_1m, index_1m, SAMPLES_1M, false, metric_getter);
rates.rate_5m =
calculateRate(current, samples_5m, index_5m, SAMPLES_5M, false, metric_getter);
rates.rate_1h =
calculateRate(current, samples_1h, index_1h, SAMPLES_1H, false, metric_getter);
rates.rate_24h =
calculateRate(current, samples_24h, index_24h, SAMPLES_24H, true, metric_getter);
return rates;
}
public:
void
addSample(SystemMetrics const& metrics)
{
auto now = std::chrono::system_clock::now();
// Update 1-minute window (every second)
samples_1m[index_1m++ % SAMPLES_1M] = metrics;
// Update 5-minute window (every second)
samples_5m[index_5m++ % SAMPLES_5M] = metrics;
// Update 1-hour window (every second)
samples_1h[index_1h++ % SAMPLES_1H] = metrics;
// Update 24-hour window (every minute)
if (last_24h_sample + std::chrono::minutes(1) <= now)
{
samples_24h[index_24h++ % SAMPLES_24H] = metrics;
last_24h_sample = now;
}
}
AllRates
getRates(SystemMetrics const& current)
{
AllRates rates;
rates.network_in = calculateMetricRates(
current, [](SystemMetrics const& m) { return m.network_bytes_in; });
rates.network_out = calculateMetricRates(
current, [](SystemMetrics const& m) { return m.network_bytes_out; });
rates.disk_read =
calculateMetricRates(current, [](SystemMetrics const& m) { return m.disk_bytes_read; });
rates.disk_write = calculateMetricRates(
current, [](SystemMetrics const& m) { return m.disk_bytes_written; });
return rates;
}
};
class DatagramMonitor
{
private:
Application& app_;
beast::Journal j_;
std::atomic<bool> running_{false};
std::thread monitor_thread_;
MetricsTracker metrics_tracker_;
struct EndpointInfo
{
std::string ip;
uint16_t port;
bool is_ipv6;
};
EndpointInfo
parseEndpoint(std::string const& endpoint)
{
auto space_pos = endpoint.find(' ');
if (space_pos == std::string::npos)
throw std::runtime_error("Invalid endpoint format");
EndpointInfo info;
info.ip = endpoint.substr(0, space_pos);
info.port = std::stoi(endpoint.substr(space_pos + 1));
info.is_ipv6 = info.ip.find(':') != std::string::npos;
return info;
}
int
createSocket(EndpointInfo const& endpoint)
{
int sock = socket(endpoint.is_ipv6 ? AF_INET6 : AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
throw std::runtime_error("Failed to create socket");
return sock;
}
void
sendPacket(int sock, EndpointInfo const& endpoint, std::vector<uint8_t> const& buffer)
{
struct sockaddr_storage addr;
socklen_t addr_len;
if (endpoint.is_ipv6)
{
struct sockaddr_in6* addr6 = reinterpret_cast<struct sockaddr_in6*>(&addr);
addr6->sin6_family = AF_INET6;
addr6->sin6_port = htons(endpoint.port);
inet_pton(AF_INET6, endpoint.ip.c_str(), &addr6->sin6_addr);
addr_len = sizeof(struct sockaddr_in6);
}
else
{
struct sockaddr_in* addr4 = reinterpret_cast<struct sockaddr_in*>(&addr);
addr4->sin_family = AF_INET;
addr4->sin_port = htons(endpoint.port);
inet_pton(AF_INET, endpoint.ip.c_str(), &addr4->sin_addr);
addr_len = sizeof(struct sockaddr_in);
}
sendto(
sock,
buffer.data(),
buffer.size(),
0,
reinterpret_cast<struct sockaddr*>(&addr),
addr_len);
}
// Returns both the counters and object count map separately
std::pair<DebugCounters, ObjectCountMap>
getDebugCounters()
{
DebugCounters counters;
ObjectCountMap objectCounts = CountedObjects::getInstance().getCounts(1);
// Database metrics if applicable
if (app_.config().useTxTables())
{
auto const db = dynamic_cast<SQLiteDatabase*>(&app_.getRelationalDatabase());
if (!db)
Throw<std::runtime_error>("Failed to get relational database");
if (auto dbKB = db->getKBUsedAll())
counters.dbKBTotal = dbKB;
if (auto dbKB = db->getKBUsedLedger())
counters.dbKBLedger = dbKB;
if (auto dbKB = db->getKBUsedTransaction())
counters.dbKBTransaction = dbKB;
if (auto count = app_.getOPs().getLocalTxCount())
counters.localTxCount = count;
}
// Basic metrics
counters.writeLoad = app_.getNodeStore().getWriteLoad();
counters.historicalPerMinute =
static_cast<std::int32_t>(app_.getInboundLedgers().fetchRate());
// Cache metrics - convert floating point rates to fixed point
counters.sleHitRate = 0; // TODO: SLE cache hit-rate accessor absent on this fork
counters.ledgerHitRate =
static_cast<std::uint32_t>(app_.getLedgerMaster().getCacheHitRate() * 1000);
counters.alSize = app_.getAcceptedLedgerCache().size();
counters.alHitRate =
static_cast<std::uint32_t>(app_.getAcceptedLedgerCache().getHitRate() * 1000);
counters.fullbelowSize =
static_cast<std::int32_t>(app_.getNodeFamily().getFullBelowCache()->size());
counters.treenodeCacheSize = app_.getNodeFamily().getTreeNodeCache()->getCacheSize();
counters.treenodeTrackSize = app_.getNodeFamily().getTreeNodeCache()->getTrackSize();
// Get regular node store metrics
counters.nodeWriteCount = app_.getNodeStore().getStoreCount();
counters.nodeWriteSize = app_.getNodeStore().getStoreSize();
counters.nodeFetchCount = app_.getNodeStore().getFetchTotalCount();
counters.nodeFetchHitCount = app_.getNodeStore().getFetchHitCount();
counters.nodeFetchSize = app_.getNodeStore().getFetchSize();
return {counters, objectCounts};
}
uint32_t
getPhysicalCPUCount()
{
static uint32_t count = 0;
if (count > 0)
return count;
#if defined(__linux__)
try
{
std::ifstream cpuinfo("/proc/cpuinfo");
if (!cpuinfo)
{
JLOG(j_.error()) << "Unable to open file: /proc/cpuinfo";
return count;
}
std::string line;
std::set<std::string> physical_ids;
std::string current_physical_id;
while (std::getline(cpuinfo, line))
{
if (line.find("core id") != std::string::npos)
{
current_physical_id = line.substr(line.find(":") + 1);
// Trim whitespace
current_physical_id.erase(0, current_physical_id.find_first_not_of(" \t"));
current_physical_id.erase(current_physical_id.find_last_not_of(" \t") + 1);
physical_ids.insert(current_physical_id);
}
}
count = physical_ids.size();
}
catch (std::exception const& e)
{
JLOG(j_.error()) << "Error getting CPU count: " << e.what();
}
// Return at least 1 if we couldn't determine the count
return count > 0 ? count : (count = 1);
#elif defined(__APPLE__)
int value = 0;
size_t size = sizeof(value);
if (sysctlbyname("hw.physicalcpu", &value, &size, NULL, 0) == 0)
count = value;
return count > 0 ? count : (count = 1);
#endif
}
SystemMetrics
collectSystemMetrics()
{
SystemMetrics metrics{};
metrics.timestamp = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
#if defined(__linux__)
// Network stats collection
try
{
std::ifstream net_file("/proc/net/dev");
if (!net_file)
{
JLOG(j_.error()) << "Unable to open file /proc/net/dev";
return metrics;
}
std::string line;
uint64_t total_bytes_in = 0, total_bytes_out = 0;
// Skip header lines
std::getline(net_file, line); // Inter-| Receive...
std::getline(net_file, line); // face |bytes...
while (std::getline(net_file, line))
{
if (line.find(':') != std::string::npos)
{
std::string interface = line.substr(0, line.find(':'));
interface = interface.substr(interface.find_first_not_of(" \t"));
interface = interface.substr(0, interface.find_last_not_of(" \t") + 1);
// Skip loopback interface
if (interface == "lo")
continue;
uint64_t bytes_in, bytes_out;
std::istringstream iss(line.substr(line.find(':') + 1));
iss >> bytes_in; // First field after : is bytes_in
for (int i = 0; i < 8; ++i)
iss >> std::ws; // Skip 8 fields
iss >> bytes_out; // 9th field is bytes_out
total_bytes_in += bytes_in;
total_bytes_out += bytes_out;
}
}
metrics.network_bytes_in = total_bytes_in;
metrics.network_bytes_out = total_bytes_out;
}
catch (std::exception const& e)
{
JLOG(j_.error()) << "Error collecting network stats: " << e.what();
}
// Disk stats collection
try
{
std::ifstream disk_file("/proc/diskstats");
if (!disk_file)
{
JLOG(j_.error()) << "Unable to open file: /proc/diskstats";
return metrics;
}
std::string line;
uint64_t total_bytes_read = 0, total_bytes_written = 0;
while (std::getline(disk_file, line))
{
unsigned int major, minor;
char dev_name[32];
uint64_t reads, read_sectors, writes, write_sectors;
if (sscanf(
line.c_str(),
"%u %u %31s %lu %*u %lu %*u %lu %*u %lu",
&major,
&minor,
dev_name,
&reads,
&read_sectors,
&writes,
&write_sectors) == 7)
{
// Only process physical devices
std::string device_name(dev_name);
if (device_name.substr(0, 3) == "dm-" || device_name.substr(0, 4) == "loop" ||
device_name.substr(0, 3) == "ram")
{
continue;
}
// Skip partitions (usually have a number at the end)
if (std::isdigit(device_name.back()))
{
continue;
}
uint64_t bytes_read = read_sectors * 512;
uint64_t bytes_written = write_sectors * 512;
total_bytes_read += bytes_read;
total_bytes_written += bytes_written;
}
}
metrics.disk_bytes_read = total_bytes_read;
metrics.disk_bytes_written = total_bytes_written;
}
catch (std::exception const& e)
{
JLOG(j_.error()) << "Error collecting disk stats: " << e.what();
}
#elif defined(__APPLE__)
// Network stats collection
try
{
struct ifaddrs* ifap;
if (getifaddrs(&ifap) == 0)
{
uint64_t total_bytes_in = 0, total_bytes_out = 0;
for (struct ifaddrs* ifa = ifap; ifa; ifa = ifa->ifa_next)
{
if (ifa->ifa_addr != NULL && ifa->ifa_addr->sa_family == AF_LINK)
{
struct if_data* ifd = (struct if_data*)ifa->ifa_data;
if (ifd != NULL)
{
// Skip loopback interface
if (strcmp(ifa->ifa_name, "lo0") == 0)
continue;
total_bytes_in += ifd->ifi_ibytes;
total_bytes_out += ifd->ifi_obytes;
}
}
}
freeifaddrs(ifap);
metrics.network_bytes_in = total_bytes_in;
metrics.network_bytes_out = total_bytes_out;
}
}
catch (std::exception const& e)
{
JLOG(j_.error()) << "Error collecting network stats: " << e.what();
}
// Disk stats collection
// Disk IO stats are not easily accessible in macOS.
// We'll set these values to zero for now.
metrics.disk_bytes_read = 0;
metrics.disk_bytes_written = 0;
#endif
return metrics;
}
std::vector<uint8_t>
generateServerInfo()
{
auto& ops = app_.getOPs();
auto& ledgerMaster = app_.getLedgerMaster();
auto currentMetrics = collectSystemMetrics();
metrics_tracker_.addSample(currentMetrics);
// Slimmed for this fork (3.2.0-b0): ledger ranges, DB debug-counters and
// the object-count map are omitted (divergent accessors). The packet is
// just the fixed header with core node + OS metrics.
std::vector<uint8_t> buffer(sizeof(ServerInfoHeader));
auto* header = reinterpret_cast<ServerInfoHeader*>(buffer.data());
memset(header, 0, sizeof(ServerInfoHeader));
header->magic = SERVER_INFO_MAGIC;
header->version = SERVER_INFO_VERSION;
header->network_id = app_.config().networkId;
header->timestamp = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
header->uptime = UptimeClock::now().time_since_epoch().count();
header->io_latency_us = app_.getIOLatency().count();
header->validation_quorum = app_.getValidators().quorum();
header->server_state = static_cast<std::uint32_t>(ops.getOperatingMode());
header->peer_count = app_.getOverlay().size();
header->node_size = app_.config().nodeSize;
auto const [counters, mode, start, initialSync] = ops.getStateAccountingData();
for (size_t i = 0; i < 5; ++i)
{
header->state_transitions[i] = counters[i].transitions;
header->state_durations[i] = counters[i].dur.count();
}
header->initial_sync_us = initialSync;
if (ops.isAmendmentBlocked())
header->warning_flags |= WARNING_AMENDMENT_BLOCKED;
if (ops.isUNLBlocked())
header->warning_flags |= WARNING_UNL_BLOCKED;
if (ops.isAmendmentWarned())
header->warning_flags |= WARNING_AMENDMENT_WARNED;
if (ops.getOperatingMode() != OperatingMode::FULL)
header->warning_flags |= WARNING_NOT_SYNCED;
header->proposer_count = ops.getPrevProposers();
header->converge_time_ms = ops.getPrevRoundTime().count();
auto const fp = ledgerMaster.getFetchPackCacheSize();
if (fp != 0)
header->fetch_pack_size = fp;
// Load factor (server only; fee-escalation term omitted on this fork).
header->load_factor = static_cast<std::uint64_t>(app_.getFeeTrack().getLoadFactor());
header->load_base = app_.getFeeTrack().getLoadBase();
#if defined(__linux__)
// Get system info using sysinfo
struct sysinfo si;
if (sysinfo(&si) == 0)
{
header->system_memory_total = si.totalram * si.mem_unit;
header->system_memory_free = si.freeram * si.mem_unit;
header->system_memory_used = header->system_memory_total - header->system_memory_free;
header->load_avg_1min = si.loads[0] / (float)(1 << SI_LOAD_SHIFT);
header->load_avg_5min = si.loads[1] / (float)(1 << SI_LOAD_SHIFT);
header->load_avg_15min = si.loads[2] / (float)(1 << SI_LOAD_SHIFT);
}
#elif defined(__APPLE__)
// Get total physical memory
int64_t physical_memory;
size_t length = sizeof(physical_memory);
if (sysctlbyname("hw.memsize", &physical_memory, &length, NULL, 0) == 0)
{
header->system_memory_total = physical_memory;
}
// Get free and used memory
vm_statistics_data_t vm_stats;
mach_msg_type_number_t count = HOST_VM_INFO_COUNT;
if (host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vm_stats, &count) ==
KERN_SUCCESS)
{
uint64_t page_size;
length = sizeof(page_size);
sysctlbyname("hw.pagesize", &page_size, &length, NULL, 0);
header->system_memory_free = (uint64_t)vm_stats.free_count * page_size;
header->system_memory_used = header->system_memory_total - header->system_memory_free;
}
// Get load averages
double loadavg[3];
if (getloadavg(loadavg, 3) == 3)
{
header->load_avg_1min = loadavg[0];
header->load_avg_5min = loadavg[1];
header->load_avg_15min = loadavg[2];
}
#endif
// Get process memory usage
struct rusage usage;
getrusage(RUSAGE_SELF, &usage);
header->process_memory_pages = usage.ru_maxrss;
// Get disk usage
#if defined(__linux__)
struct statvfs fs;
if (statvfs("/", &fs) == 0)
{
header->system_disk_total = fs.f_blocks * fs.f_frsize;
header->system_disk_free = fs.f_bfree * fs.f_frsize;
header->system_disk_used = header->system_disk_total - header->system_disk_free;
}
#elif defined(__APPLE__)
struct statfs fs;
if (statfs("/", &fs) == 0)
{
header->system_disk_total = fs.f_blocks * fs.f_bsize;
header->system_disk_free = fs.f_bfree * fs.f_bsize;
header->system_disk_used = header->system_disk_total - header->system_disk_free;
}
#endif
// Get CPU core count
header->cpu_cores = getPhysicalCPUCount();
// Get rate statistics
auto rates = metrics_tracker_.getRates(currentMetrics);
header->rates.network_in = rates.network_in;
header->rates.network_out = rates.network_out;
header->rates.disk_read = rates.disk_read;
header->rates.disk_write = rates.disk_write;
// Ledger height + hash via stable accessors (this fork's Ledger lacks
// info()). The hash lets the collector detect a fork: divergent
// ledger_hash across nodes at the same ledger_seq.
std::uint32_t const validSeq = ledgerMaster.getValidLedgerIndex();
header->ledger_seq = validSeq;
uint256 const validHash = ledgerMaster.getHashBySeq(validSeq);
std::memcpy(header->ledger_hash, validHash.data(), 32);
header->reserve_base = app_.config().fees.accountReserve.drops();
header->reserve_inc = app_.config().fees.ownerReserve.drops();
// Node public key + version string.
auto const& nodeKey = app_.nodeIdentity().first;
std::memcpy(header->node_public_key, nodeKey.data(), 33);
memset(&header->version_string, 0, 32);
memcpy(
&header->version_string,
build_info::getVersionString().c_str(),
build_info::getVersionString().size() > 32 ? 32
: build_info::getVersionString().size());
header->ledger_range_count = 0;
return buffer;
}
void
monitorThread()
{
std::vector<std::pair<EndpointInfo, int>> endpoints;
for (auto const& epStr : app_.config().DATAGRAM_MONITOR)
{
auto endpoint = parseEndpoint(epStr);
endpoints.push_back(std::make_pair(endpoint, createSocket(endpoint)));
}
while (running_)
{
try
{
auto info = generateServerInfo();
for (auto const& ep : endpoints)
{
sendPacket(ep.second, ep.first, info);
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
catch (std::exception const& e)
{
// Log error but continue monitoring
JLOG(j_.error()) << "Server info monitor error: " << e.what();
}
}
for (auto const& ep : endpoints)
{
close(ep.second);
}
}
public:
DatagramMonitor(Application& app) : app_(app), j_(beast::Journal::getNullSink())
{
}
void
start()
{
if (!running_.exchange(true))
{
monitor_thread_ = std::thread(&DatagramMonitor::monitorThread, this);
}
}
void
stop()
{
if (running_.exchange(false))
{
if (monitor_thread_.joinable())
monitor_thread_.join();
}
}
~DatagramMonitor()
{
stop();
}
};
} // namespace xrpl

View File

@@ -356,6 +356,9 @@ public:
OperatingMode
getOperatingMode() const override;
StateAccountingData
getStateAccountingData() override;
std::string
strOperatingMode(OperatingMode const mode, bool const admin) const override;
@@ -521,6 +524,10 @@ public:
json::Value
getConsensusInfo() override;
std::size_t
getPrevProposers() const override;
std::chrono::milliseconds
getPrevRoundTime() const override;
json::Value
getServerInfo(bool human, bool admin, bool counters) override;
void
@@ -1091,6 +1098,16 @@ NetworkOPsImp::getOperatingMode() const
return mode_;
}
NetworkOPs::StateAccountingData
NetworkOPsImp::getStateAccountingData()
{
auto const data = accounting_.getCounterData();
std::array<NetworkOPs::AccountingCounter, 5> out;
for (std::size_t i = 0; i < out.size(); ++i)
out[i] = {data.counters[i].transitions, data.counters[i].dur};
return {out, data.mode, data.start, data.initialSyncUs};
}
inline std::string
NetworkOPsImp::strOperatingMode(bool const admin /* = false */) const
{
@@ -2804,6 +2821,18 @@ NetworkOPsImp::getConsensusInfo()
return consensus_.getJson(true);
}
std::size_t
NetworkOPsImp::getPrevProposers() const
{
return consensus_.prevProposers();
}
std::chrono::milliseconds
NetworkOPsImp::getPrevRoundTime() const
{
return consensus_.prevRoundTime();
}
json::Value
NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
{

View File

@@ -150,6 +150,10 @@ public:
// Entries from [ips_fixed] config stanza
std::vector<std::string> ipsFixed;
// Entries from [datagram_monitor]: "<IP> <port>" UDP targets the
// DatagramMonitor sends node-stats packets to (XDGM, every 1s).
std::vector<std::string> DATAGRAM_MONITOR;
StartUpType startUp = StartUpType::Normal;
bool startValid = false;

View File

@@ -481,6 +481,9 @@ Config::loadFromString(std::string const& fileContents)
if (auto s = getIniFileSection(secConfig, Sections::kIpsFixed))
ipsFixed = *s;
if (auto s = getIniFileSection(secConfig, Sections::kDatagramMonitor))
DATAGRAM_MONITOR = *s;
// if the user has specified ip:port then replace : with a space.
{
auto replaceColons = [](std::vector<std::string>& strVec) {