Merge remote-tracking branch 'origin/develop' into pratik/otel-phase1a-plan-docs

This commit is contained in:
Pratik Mankawde
2026-09-15 16:17:43 +01:00
62 changed files with 2384 additions and 179 deletions

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

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

@@ -41,6 +41,7 @@ Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta
- `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655)
- `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728)
- `ledger`: `delivered_amount` is now included in the metadata of successful `AccountDelete` transactions when transactions are expanded (`expand`, or admin-only `full`). Previously it was only added for `Payment` and `CheckCash`, which made `ledger` inconsistent with `tx` and `account_tx`. [#5706](https://github.com/XRPLF/rippled/pull/5706)
- `noripple_check`: The `transactions` field is no longer included in error responses; it is still returned (possibly as an empty array) whenever `transactions` is `true` and the request succeeds. A malformed `account` is now rejected before the ledger is looked up, so that error response no longer carries the `ledger_hash`, `ledger_index`, and `validated` fields ([#6303](https://github.com/XRPLF/rippled/pull/6303)).
## XRP Ledger server version 3.3.0

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

@@ -26,7 +26,8 @@ struct Config
/**
* The largest number of public peer slots to allow.
* This includes both inbound and outbound, but does not include
* fixed peers.
* fixed peers. A configuration built by `makeConfig` always holds
* `maxPeers == inPeers + outPeers`.
*/
std::size_t maxPeers{tuning::kDefaultMaxPeers};

View File

@@ -6,6 +6,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STInteger.h> // IWYU pragma: keep
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
@@ -301,6 +302,63 @@ verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 co
NotTEC
checkEncryptedAmountFormat(STObject const& object);
/**
* @brief Checks whether a holder's issuer mirror is encrypted under the
* issuance's currently registered issuer key.
*
* Verifies that the holder's issuer mirror epoch matches the active issuer key
* epoch on the issuance. An absent mirror epoch defaults to epoch 0. A holder without an issuer
* mirror is considered stale, as there is no key anchor for future re-encryptions.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger object.
* @return true if the MPToken's issuer mirror is current. false if stale.
*/
[[nodiscard]] bool
isIssuerMirrorCurrent(SLE const& issuance, SLE const& mptoken);
/**
* @brief Checks whether a holder's auditor mirror is encrypted under the
* issuance's currently registered auditor key.
*
* Verifies that the holder's auditor mirror epoch matches the active auditor key
* epoch on the issuance. An absent mirror epoch defaults to epoch 0. An issuance
* without an auditor key requires no auditor mirror and is considered current.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger object.
* @return true if the auditor mirror is current or not required.
*/
[[nodiscard]] bool
isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken);
/**
* @brief Checks whether each mirror a holder is required to have is encrypted
* under the issuance's currently registered ElGamal keys.
*
* Verifies that both the issuer mirror and the auditor mirror (if required)
* are current. This serves as a combined check, ensuring all necessary
* holder mirror epochs match the active key epochs on the issuance.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger object.
* @return true if the required mirrors are current.
*/
[[nodiscard]] bool
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken);
/**
* @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs.
*
* Call this after writing mirror ciphertexts under the issuance's currently
* registered keys, so that the mirrors read as current afterwards.
*
* @param issuance The MPTokenIssuance ledger object.
* @param mptoken The holder's MPToken ledger entry to update.
*/
void
setMirrorEpochs(SLE const& issuance, SLE& mptoken);
/**
* @brief Verifies revealed amount encryptions for all recipients.
*

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,8 @@
// 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)

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},
@@ -429,6 +436,8 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({
{sfConfidentialBalanceVersion, SoeDefault},
{sfIssuerEncryptedBalance, SoeOptional},
{sfAuditorEncryptedBalance, SoeOptional},
{sfIssuerKeyMirrorEpoch, SoeOptional},
{sfAuditorKeyMirrorEpoch, SoeOptional},
{sfHolderEncryptionKey, SoeOptional},
}))

View File

@@ -123,6 +123,11 @@ 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)
@@ -238,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)
@@ -322,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

@@ -278,6 +278,7 @@ JSS(frozen_balances); // out: GatewayBalances
JSS(full); // in: LedgerClearer, handlers/Ledger
JSS(full_reply); // out: PathFind
JSS(fullbelow_size); // out: GetCounts
JSS(gateway); // in: noripple_check
JSS(git); // out: server_info
JSS(good); // out: RPCVersion
JSS(hash); // out: NetworkOPs, InboundLedger, LedgerToJson, STTx; field
@@ -481,6 +482,7 @@ JSS(ports); // out: NetworkOPs
JSS(previous); // out: Reservations
JSS(previous_ledger); // out: LedgerPropose
JSS(price); // out: amm_info, AuctionSlot
JSS(problems); // out: noripple_check
JSS(proof); // in: BookOffers
JSS(propose_seq); // out: LedgerPropose
JSS(proposers); // out: NetworkOPs, LedgerConsensus
@@ -660,6 +662,7 @@ JSS(url); // in/out: Subscribe, Unsubscribe
JSS(url_password); // in: Subscribe
JSS(url_username); // in: Subscribe
JSS(urlgravatar); //
JSS(user); // in: noripple_check
JSS(username); // in: Subscribe
JSS(validated); // out: NetworkOPs, RPCHelpers, AccountTx*, Tx
JSS(validator_list_expires); // out: NetworkOps, ValidatorList

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

@@ -268,6 +268,54 @@ public:
return this->sle_->isFieldPresent(sfAuditorEncryptedBalance);
}
/**
* @brief Get sfIssuerKeyMirrorEpoch (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getIssuerKeyMirrorEpoch() const
{
if (hasIssuerKeyMirrorEpoch())
return this->sle_->at(sfIssuerKeyMirrorEpoch);
return std::nullopt;
}
/**
* @brief Check if sfIssuerKeyMirrorEpoch is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasIssuerKeyMirrorEpoch() const
{
return this->sle_->isFieldPresent(sfIssuerKeyMirrorEpoch);
}
/**
* @brief Get sfAuditorKeyMirrorEpoch (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_UINT32::type::value_type>
getAuditorKeyMirrorEpoch() const
{
if (hasAuditorKeyMirrorEpoch())
return this->sle_->at(sfAuditorKeyMirrorEpoch);
return std::nullopt;
}
/**
* @brief Check if sfAuditorKeyMirrorEpoch is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasAuditorKeyMirrorEpoch() const
{
return this->sle_->isFieldPresent(sfAuditorKeyMirrorEpoch);
}
/**
* @brief Get sfHolderEncryptionKey (SoeOptional)
* @return The field value, or std::nullopt if not present.
@@ -471,6 +519,28 @@ public:
return *this;
}
/**
* @brief Set sfIssuerKeyMirrorEpoch (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setIssuerKeyMirrorEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfIssuerKeyMirrorEpoch] = value;
return *this;
}
/**
* @brief Set sfAuditorKeyMirrorEpoch (SoeOptional)
* @return Reference to this builder for method chaining.
*/
MPTokenBuilder&
setAuditorKeyMirrorEpoch(std::decay_t<typename SF_UINT32::type::value_type> const& value)
{
object_[sfAuditorKeyMirrorEpoch] = value;
return *this;
}
/**
* @brief Set sfHolderEncryptionKey (SoeOptional)
* @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

@@ -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

@@ -107,8 +107,12 @@ Config::makeConfig(
else
{
config.outPeers = *limits.outPeers;
config.inPeers = *limits.inPeers;
config.maxPeers = 0;
// Inbound slots only exist if we accept incoming connections, and
// `maxPeers` is the total across both directions. The legacy branch
// above upholds the same two invariants.
config.inPeers = config.wantIncoming ? *limits.inPeers : 0;
config.maxPeers = config.inPeers + config.outPeers;
}
// This will cause servers configured as validators to request that

View File

@@ -6,9 +6,11 @@
#include <xrpl/basics/contract.h>
#include <xrpl/beast/utility/instrumentation.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STBlob.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/UintTypes.h>
@@ -397,6 +399,59 @@ checkEncryptedAmountFormat(STObject const& object)
return tesSUCCESS;
}
bool
isIssuerMirrorCurrent(SLE const& issuance, SLE const& mptoken)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::isIssuerMirrorCurrent : issuance MPTokenIssuance object");
XRPL_ASSERT(
mptoken.getType() == ltMPTOKEN, "xrpl::isIssuerMirrorCurrent : mptoken MPToken object");
return mptoken.isFieldPresent(sfIssuerEncryptedBalance) &&
mptoken[~sfIssuerKeyMirrorEpoch].value_or(0) == issuance[~sfIssuerKeyEpoch].value_or(0);
}
bool
isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::isAuditorMirrorCurrent : issuance MPTokenIssuance object");
XRPL_ASSERT(
mptoken.getType() == ltMPTOKEN, "xrpl::isAuditorMirrorCurrent : mptoken MPToken object");
if (!issuance.isFieldPresent(sfAuditorEncryptionKey))
return true;
return mptoken.isFieldPresent(sfAuditorEncryptedBalance) &&
mptoken[~sfAuditorKeyMirrorEpoch].value_or(0) == issuance[~sfAuditorKeyEpoch].value_or(0);
}
bool
areMirrorsCurrent(SLE const& issuance, SLE const& mptoken)
{
return isIssuerMirrorCurrent(issuance, mptoken) && isAuditorMirrorCurrent(issuance, mptoken);
}
void
setMirrorEpochs(SLE const& issuance, SLE& mptoken)
{
XRPL_ASSERT(
issuance.getType() == ltMPTOKEN_ISSUANCE,
"xrpl::setMirrorEpochs : issuance MPTokenIssuance object");
XRPL_ASSERT(mptoken.getType() == ltMPTOKEN, "xrpl::setMirrorEpochs : mptoken MPToken object");
if (auto const epoch = issuance[~sfIssuerKeyEpoch].value_or(0); epoch != 0)
mptoken[sfIssuerKeyMirrorEpoch] = epoch;
if (mptoken.isFieldPresent(sfAuditorEncryptedBalance))
{
if (auto const epoch = issuance[~sfAuditorKeyEpoch].value_or(0); epoch != 0)
mptoken[sfAuditorKeyMirrorEpoch] = epoch;
}
}
TER
verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash)
{

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

@@ -5,6 +5,7 @@
#include <xrpl/core/ServiceRegistry.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
@@ -209,6 +210,12 @@ ConfidentialMPTClawback::doApply()
(*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor);
}
// Allow clawback on stale mirrors since the issuer can still generate the
// proof using the corresponding stale private key. The mirrors are updated
// to the current epoch during execution.
if (view().rules().enabled(featureConfidentialMPTKeyRotation))
setMirrorEpochs(*sleIssuance, *sleHolderMPToken);
// Decrease Global Confidential Outstanding Amount
auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount];
if (clawAmount > oldCOA)

View File

@@ -8,6 +8,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/MPTIssue.h>
@@ -111,6 +112,17 @@ ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx)
if (!sleMptoken)
return tecOBJECT_NOT_FOUND;
// An already-initialized holder has their new ciphertexts homomorphically
// added to their existing mirrors, so those mirrors must be encrypted under
// the currently registered keys. A first-time convert creates the mirrors
// under those keys instead, and has nothing to be stale.
if (ctx.view.rules().enabled(featureConfidentialMPTKeyRotation) &&
sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) &&
!areMirrorsCurrent(*sleIssuance, *sleMptoken))
{
return tecNO_PERMISSION;
}
auto const mptIssue = MPTIssue{issuanceID};
// Explicit freeze and auth checks are required because accountHolds
@@ -331,6 +343,10 @@ ConfidentialMPTConvert::doApply()
if (auditorEc)
(*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc;
// Initialize key epochs when registering the keys.
if (view().rules().enabled(featureConfidentialMPTKeyRotation))
setMirrorEpochs(*sleIssuance, *sleMptoken);
// Spending balance starts at zero. Must use canonical zero encryption
// (deterministic ciphertext) so the ledger state is reproducible.
auto zeroBalance = encryptCanonicalZeroAmount(

View File

@@ -7,6 +7,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
@@ -197,6 +198,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
// Converting back homomorphically subtracts from the holder's mirrors, so
// those mirrors must be current.
if (ctx.view.rules().enabled(featureConfidentialMPTKeyRotation) &&
!areMirrorsCurrent(*sleIssuance, *sleMptoken))
{
return tecNO_PERMISSION;
}
// Sanity check: holder's MPToken must have auditor balance field if auditing
// is enabled
if (requiresAuditor && !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance))

View File

@@ -247,6 +247,15 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
// A send homomorphically updates the mirrors of both parties, so both must
// be current.
if (ctx.view.rules().enabled(featureConfidentialMPTKeyRotation) &&
(!areMirrorsCurrent(*sleIssuance, *sleSenderMPToken) ||
!areMirrorsCurrent(*sleIssuance, *sleDestinationMPToken)))
{
return tecNO_PERMISSION;
}
// Sanity check: Both MPTokens' auditor fields must be present if auditing
// is enabled
if (requiresAuditor &&

View File

@@ -17,6 +17,7 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <vector>
namespace xrpl {
@@ -604,6 +605,661 @@ class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase
BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch));
}
void
testConfidentialMPTConvertEpoch(FeatureBitset features)
{
testcase("ConfidentialMPTConvert mirror epoch");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
Account const auditor("auditor");
// A first-time convert with no rotation leaves both mirror
// epochs absent.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor});
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
// Both mirrors are current, so converting again is allowed and
// leaves the epochs untouched.
mptAlice.convert({
.account = bob,
.amt = 20,
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
}
// Every remaining case needs key rotation to be enabled.
if (!features[featureConfidentialMPTKeyRotation])
return;
// A first-time convert stamps the mirrors with whatever epochs the
// issuance currently sits at. Only issuer key rotated in this case.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// Ten rotations, issuance's issuer epoch is 10.
for (int i = 0; i < 10; ++i)
{
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
}
BEAST_EXPECT(mptAlice.checkKeyEpochs(10u, std::nullopt));
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
// carol converts for the first time, and her mirrors are stamped with the current
// issuer epoch of 10.
mptAlice.convert({
.account = carol,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(carol),
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 10u, std::nullopt));
}
// A first-time convert stamps the mirrors with whatever epochs the
// issuance currently sits at. Both keys rotated in this case.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// 100 rotations of both keys, so both epochs are 100.
for (int i = 0; i < 100; ++i)
{
mptAlice.generateKeyPair(alice);
mptAlice.generateKeyPair(auditor);
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
}
// 5 more rotations of the auditor key alone, so the auditor epoch is 105 now.
for (int i = 0; i < 5; ++i)
{
mptAlice.generateKeyPair(auditor);
mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
}
BEAST_EXPECT(mptAlice.checkKeyEpochs(100u, 105u));
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
// carol converts for the first time, and each of her mirrors is stamped with the epoch
// of the key it was encrypted under.
mptAlice.convert({
.account = carol,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(carol),
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 100u, 105u));
}
// An issuer key rotation leaves an existing holder's issuer mirror
// behind, converting will be blocked until the holder's mirror is updated to the new epoch.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// carol initializes before any rotation, so her mirrors carry no epoch
// at all, the state every holder is in before the amendment.
mptAlice.convert({
.account = carol,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(carol),
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
// Rotate the issuer key to epoch 1.
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
// bob converts for the first time which is allowed when registering the key.
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
// carol's absent epoch reads as 0 which is stale.
mptAlice.convert({
.account = carol,
.amt = 20,
.err = tecNO_PERMISSION,
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
// Rotate the issuer key to epoch 2, leaving bob's issuer mirror stale.
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, std::nullopt));
// This is not the first time convert, and bob's issuer mirror is behind the current
// epoch, so the convert is rejected.
mptAlice.convert({
.account = bob,
.amt = 20,
.err = tecNO_PERMISSION,
});
// The rejected convert leaves bob's mirrors exactly as they were.
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
// carol still cannot convert.
mptAlice.convert({
.account = carol,
.amt = 20,
.err = tecNO_PERMISSION,
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
}
// The auditor mirror is checked the same way, so rotating only the
// auditor key blocks the convert on its own, with the issuer epoch
// untouched.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// bob initializes his confidential balance at epoch 0, so both of his
// mirrors are current.
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
// Rotate the auditor key only, leaving bob's auditor mirror behind
// while his issuer mirror stays current.
mptAlice.generateKeyPair(auditor);
mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor));
mptAlice.convert({
.account = bob,
.amt = 20,
.err = tecNO_PERMISSION,
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
// Carol converts for the first time, and her auditor mirror is stamped with the current
// auditor epoch of 1.
mptAlice.convert({
.account = carol,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(carol),
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, 1u));
}
// A late auditor key registration bumps no epoch.
// Although both epochs are still zero, the convert is blocked
// because auditor mirror is missing.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor});
// Register the issuer key only.
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
});
// The issuance has no auditor yet, so no auditor mirror is created.
mptAlice.convert({
.account = bob,
.amt = 50,
.fillAuditorEncryptedAmt = false,
.holderPubKey = mptAlice.getPubKey(bob),
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
// Register the auditor key later, which bumps no epoch.
mptAlice.set({
.account = alice,
.auditorPubKey = mptAlice.getPubKey(auditor),
});
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt));
// bob's auditor mirror is still missing, so the convert is rejected.
mptAlice.convert({
.account = bob,
.amt = 20,
.err = tecNO_PERMISSION,
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
}
}
void
testConfidentialMPTSendEpoch(FeatureBitset features)
{
testcase("ConfidentialMPTSend mirror epoch");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const carol("carol");
Account const auditor("auditor");
// Two holders that both initialized after a rotation are current, so a
// send between them succeeds and leaves both mirrors untouched.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
setupConfidentialIssuance(mptAlice, alice, {bob, carol});
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
// Rotate the issuer key to epoch 1 before anyone holds a confidential
// balance.
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
for (auto const& holder : {bob, carol})
{
mptAlice.convert({
.account = holder,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(holder),
});
mptAlice.mergeInbox({.account = holder});
}
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 1u, std::nullopt));
mptAlice.send({.account = bob, .dest = carol, .amt = 10});
// The epochs are unchanged after send.
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 1u, std::nullopt));
}
// Either the sender or the destination being stale will be rejected.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}});
setupConfidentialIssuance(mptAlice, alice, {bob, carol});
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
// carol initializes at epoch 0.
mptAlice.convert({
.account = carol,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(carol),
});
mptAlice.mergeInbox({.account = carol});
// Rotate the issuer key to epoch 1, leaving carol behind.
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
// bob initializes after the rotation, so his mirrors are current.
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
mptAlice.mergeInbox({.account = bob});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
// This is rejected because the sender is the stale even though the destination is
// current.
mptAlice.send({
.account = carol,
.dest = bob,
.amt = 10,
.err = tecNO_PERMISSION,
});
// This is rejected because the destination is the stale even though the sender is
// current.
mptAlice.send({
.account = bob,
.dest = carol,
.amt = 10,
.err = tecNO_PERMISSION,
});
// The rejected sends leave both mirrors as they were.
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
}
// Auditor mirror is stale, the send will be rejected.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor});
setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor});
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
for (auto const& holder : {bob, carol})
{
mptAlice.convert({
.account = holder,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(holder),
});
mptAlice.mergeInbox({.account = holder});
}
// Rotate the auditor key only
mptAlice.generateKeyPair(auditor);
mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
mptAlice.send({
.account = bob,
.dest = carol,
.amt = 10,
.err = tecNO_PERMISSION,
});
mptAlice.send({
.account = carol,
.dest = bob,
.amt = 10,
.err = tecNO_PERMISSION,
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt));
}
}
void
testConfidentialMPTConvertBackEpoch(FeatureBitset features)
{
testcase("ConfidentialMPTConvertBack mirror epoch");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const auditor("auditor");
// A holder who initialized after a rotation is current, so converting
// back is allowed and leaves the epoch it was stamped with alone.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
setupConfidentialIssuance(mptAlice, alice, {bob});
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
// Rotate the issuer key to epoch 1 before bob holds a confidential
// balance.
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
mptAlice.mergeInbox({.account = bob});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
mptAlice.convertBack({.account = bob, .amt = 20});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt));
}
// Converting back with stale mirrors is rejected.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
setupConfidentialIssuance(mptAlice, alice, {bob});
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
// bob initializes at epoch 0.
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
mptAlice.mergeInbox({.account = bob});
// Converting back is allowed while his mirrors are still current.
mptAlice.convertBack({.account = bob, .amt = 20});
// Rotate the issuer key to epoch 1, leaving bob behind.
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
mptAlice.convertBack({
.account = bob,
.amt = 10,
.err = tecNO_PERMISSION,
});
// The rejected convert back leaves bob's mirrors as they were.
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
}
// Converting back with a stale auditor mirror is rejected, even if the issuer mirror is
// current.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor});
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
mptAlice.mergeInbox({.account = bob});
// Rotate the auditor key only, leaving bob behind on that mirror alone.
mptAlice.generateKeyPair(auditor);
mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u));
mptAlice.convertBack({
.account = bob,
.amt = 10,
.err = tecNO_PERMISSION,
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
}
}
void
testConfidentialMPTClawbackEpoch(FeatureBitset features)
{
testcase("ConfidentialMPTClawback mirror epoch");
using namespace test::jtx;
Account const alice("alice");
Account const bob("bob");
Account const auditor("auditor");
std::uint32_t const clawbackFlags =
tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback;
// Clawback is not blocked on
// a stale auditor mirror.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor});
setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor}, clawbackFlags);
mptAlice.set({
.account = alice,
.issuerPubKey = mptAlice.getPubKey(alice),
.auditorPubKey = mptAlice.getPubKey(auditor),
});
// bob initializes both mirrors at epoch 0.
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
// Rotate the auditor key twice, leaving bob's auditor mirror behind.
for (int i = 0; i < 2; ++i)
{
mptAlice.generateKeyPair(auditor);
mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)});
}
BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 2u));
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
mptAlice.confidentialClaw({.account = alice, .holder = bob, .amt = 50});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, 2u));
}
// A holder who initialized after a rotation is clawed back successfully, and
// the issuer mirror is updated to the current epoch.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
setupConfidentialIssuance(mptAlice, alice, {bob}, {}, clawbackFlags);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
// Rotate the issuer key five times, issuance's issuer epoch is 5.
for (int i = 0; i < 5; ++i)
{
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
}
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 5u, std::nullopt));
mptAlice.confidentialClaw({.account = alice, .holder = bob, .amt = 50});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 5u, std::nullopt));
}
// Clawback is not blocked on
// a stale issuer mirror. For now the proof cannot verify: it is checked
// against the key registered on the issuance, while the mirror is still
// encrypted under the key it was written with, and that older key is
// nowhere on the ledger yet. This will be added in a separate PR.
{
Env env{*this, features};
MPTTester mptAlice(env, alice, {.holders = {bob}});
setupConfidentialIssuance(mptAlice, alice, {bob}, {}, clawbackFlags);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
// bob initializes at epoch 0.
mptAlice.convert({
.account = bob,
.amt = 50,
.holderPubKey = mptAlice.getPubKey(bob),
});
// Rotate the issuer key to epoch 1, leaving bob behind.
mptAlice.generateKeyPair(alice);
mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)});
BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt));
mptAlice.confidentialClaw({
.account = alice,
.holder = bob,
.amt = 50,
.err = tecBAD_PROOF,
});
BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt));
}
}
public:
void
testMPTokenIssuanceSetWithFeats(FeatureBitset features)
{
@@ -626,6 +1282,12 @@ public:
testMPTokenIssuanceSetWithFeats(all);
testMPTokenIssuanceSetWithFeats(all - featureConfidentialMPTKeyRotation);
testConfidentialMPTConvertEpoch(all);
testConfidentialMPTConvertEpoch(all - featureConfidentialMPTKeyRotation);
testConfidentialMPTSendEpoch(all);
testConfidentialMPTConvertBackEpoch(all);
testConfidentialMPTClawbackEpoch(all);
}
};

View File

@@ -418,6 +418,18 @@ protected:
}
};
// Create an issuance that can hold confidential balances, with the listed
// holders funded and authorized, and a key pair generated for the issuer,
// every holder, and every extra key owner. The keys are
// generated but not registered.
static void
setupConfidentialIssuance(
test::jtx::MPTTester& mpt,
test::jtx::Account const& issuer,
std::vector<test::jtx::Account> const& holders,
std::vector<test::jtx::Account> const& keyOwners = {},
std::uint32_t flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance);
// Set up an MPT environment suitable for batch testing.
// alice is issuer; bob has 'bobAmt' in confidential spending; carol has
// 'carolAmt' in confidential spending; dave is initialised with pubkey but

View File

@@ -0,0 +1,37 @@
#include <test/jtx/ConfidentialTransfer.h>
#include <test/jtx/Account.h>
#include <test/jtx/mpt.h>
#include <cstdint>
#include <vector>
namespace xrpl {
void
ConfidentialTransferTestBase::setupConfidentialIssuance(
test::jtx::MPTTester& mpt,
test::jtx::Account const& issuer,
std::vector<test::jtx::Account> const& holders,
std::vector<test::jtx::Account> const& keyOwners,
std::uint32_t flags)
{
using namespace test::jtx;
mpt.create({
.ownerCount = 1,
.flags = flags,
});
for (auto const& holder : holders)
{
mpt.authorize({.account = holder});
mpt.pay(issuer, holder, 100);
mpt.generateKeyPair(holder);
}
mpt.generateKeyPair(issuer);
for (auto const& keyOwner : keyOwners)
mpt.generateKeyPair(keyOwner);
}
} // namespace xrpl

View File

@@ -92,6 +92,33 @@ makePedersenParams(PedersenProofParams const& params)
return res;
}
/**
* @brief Looks up an account's key at a given key epoch.
*
* @param keys accounts' history of keys, indexed by key epoch.
* @param account The account whose key is being looked up.
* @param epoch The key epoch, or std::nullopt for the account's latest key.
* @return The key, or std::nullopt if the account has no key at that epoch.
*/
[[nodiscard]] std::optional<Buffer>
keyAtEpoch(
std::unordered_map<AccountID, std::vector<Buffer>> const& keys,
AccountID const& account,
std::optional<std::uint32_t> epoch)
{
auto const it = keys.find(account);
if (it == keys.end() || it->second.empty())
return std::nullopt;
if (!epoch)
return it->second.back();
if (*epoch >= it->second.size())
return std::nullopt;
return it->second[*epoch];
}
} // namespace
void
@@ -659,6 +686,33 @@ MPTTester::checkKeyEpochs(
});
}
[[nodiscard]] bool
MPTTester::checkMirrorEpochs(
Account const& holder,
std::optional<std::uint32_t> issuerKeyMirrorEpoch,
std::optional<std::uint32_t> auditorKeyMirrorEpoch) const
{
return forObject(
[&](SLEP const& sle) -> bool {
return (*sle)[~sfIssuerKeyMirrorEpoch] == issuerKeyMirrorEpoch &&
(*sle)[~sfAuditorKeyMirrorEpoch] == auditorKeyMirrorEpoch;
},
holder);
}
[[nodiscard]] std::optional<std::uint32_t>
MPTTester::getMirrorEpoch(Account const& holder, SF_UINT32 const& field) const
{
std::optional<std::uint32_t> epoch;
forObject(
[&](SLEP const& sle) -> bool {
epoch = (*sle)[~field];
return true;
},
holder);
return epoch;
}
[[nodiscard]] bool
MPTTester::checkEncryptionKeys(
std::optional<Account> const& issuerKeyOwner,
@@ -1172,8 +1226,13 @@ MPTTester::convert(MPTConvert const& arg)
if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance)
Throw<std::runtime_error>("Failed to get Pre-convert balance");
// The auditor mirror is only touched if the transaction carries an auditor
// ciphertext, which mirrors the condition convertJV fills it under.
bool const hasAuditorAmt =
arg.auditorEncryptedAmt || (auditor_ && arg.fillAuditorEncryptedAmt.value_or(false));
std::optional<uint64_t> prevAuditorBalance;
if (arg.auditorEncryptedAmt || auditor_)
if (hasAuditorAmt)
{
prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance);
if (!prevAuditorBalance)
@@ -1212,7 +1271,7 @@ MPTTester::convert(MPTConvert const& arg)
if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance)
Throw<std::runtime_error>("Failed to get post-convert balance");
if (arg.auditorEncryptedAmt || auditor_)
if (hasAuditorAmt)
{
auto const postAuditorBalance =
getDecryptedBalance(*arg.account, auditorEncryptedBalance);
@@ -2039,7 +2098,7 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg)
}
}
void
std::uint32_t
MPTTester::generateKeyPair(Account const& account)
{
unsigned char privKey[kEcPrivKeyLength];
@@ -2057,26 +2116,23 @@ MPTTester::generateKeyPair(Account const& account)
Throw<std::runtime_error>("failed to serialize public key");
}
pubKeys_.insert({account.id(), Buffer{compressedPubKey, kEcPubKeyLength}});
privKeys_.insert({account.id(), Buffer{privKey, kEcPrivKeyLength}});
auto& pubKeyEpochs = pubKeys_[account.id()];
pubKeyEpochs.emplace_back(compressedPubKey, kEcPubKeyLength);
privKeys_[account.id()].emplace_back(privKey, kEcPrivKeyLength);
return static_cast<std::uint32_t>(pubKeyEpochs.size() - 1);
}
std::optional<Buffer>
MPTTester::getPubKey(Account const& account) const
MPTTester::getPubKey(Account const& account, std::optional<std::uint32_t> epoch) const
{
if (auto const it = pubKeys_.find(account.id()); it != pubKeys_.end())
return it->second;
return std::nullopt;
return keyAtEpoch(pubKeys_, account.id(), epoch);
}
std::optional<Buffer>
MPTTester::getPrivKey(Account const& account) const
MPTTester::getPrivKey(Account const& account, std::optional<std::uint32_t> epoch) const
{
if (auto const it = privKeys_.find(account.id()); it != privKeys_.end())
return it->second;
return std::nullopt;
return keyAtEpoch(privKeys_, account.id(), epoch);
}
Buffer
@@ -2095,7 +2151,10 @@ MPTTester::encryptAmount(Account const& account, uint64_t const amt, Buffer cons
}
std::optional<uint64_t>
MPTTester::decryptAmount(Account const& account, Buffer const& amt) const
MPTTester::decryptAmount(
Account const& account,
Buffer const& amt,
std::optional<std::uint32_t> epoch) const
{
if (amt.size() != kEcGamalEncryptedTotalLength)
return std::nullopt;
@@ -2104,7 +2163,7 @@ MPTTester::decryptAmount(Account const& account, Buffer const& amt) const
if (!pair)
return std::nullopt;
auto const privKey = getPrivKey(account);
auto const privKey = getPrivKey(account, epoch);
if (!privKey || privKey->size() != kEcPrivKeyLength)
return std::nullopt;
@@ -2136,18 +2195,24 @@ MPTTester::getDecryptedBalance(Account const& account, EncryptedBalanceType bala
Account decryptor = account;
// A mirror stays encrypted under the key it was written with, so a rotation
// leaves it readable only by that generation of the key, not the latest one.
std::optional<std::uint32_t> epoch;
if (balanceType == issuerEncryptedBalance)
{
decryptor = issuer_;
epoch = getMirrorEpoch(account, sfIssuerKeyMirrorEpoch).value_or(0);
}
else if (balanceType == auditorEncryptedBalance)
{
if (!auditor_)
return std::nullopt;
decryptor = *auditor_;
epoch = getMirrorEpoch(account, sfAuditorKeyMirrorEpoch).value_or(0);
}
return decryptAmount(decryptor, *encryptedAmt);
return decryptAmount(decryptor, *encryptedAmt, epoch);
};
json::Value

View File

@@ -20,6 +20,7 @@
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/ConfidentialTransfer.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
@@ -451,8 +452,10 @@ class MPTTester
std::optional<Account> const auditor_;
std::optional<MPTID> id_;
bool close_;
std::unordered_map<AccountID, Buffer> pubKeys_;
std::unordered_map<AccountID, Buffer> privKeys_;
// Keys generated for each account. Buffer vector's index is the key epoch: index 0 is
// the initial pair and each rotation appends.
std::unordered_map<AccountID, std::vector<Buffer>> pubKeys_;
std::unordered_map<AccountID, std::vector<Buffer>> privKeys_;
public:
enum class EncryptedBalanceType {
@@ -619,6 +622,15 @@ public:
std::optional<std::uint32_t> issuerKeyEpoch,
std::optional<std::uint32_t> auditorKeyEpoch) const;
// Checks both mirror epochs on a holder's MPToken. Pass std::nullopt for an
// epoch that is expected to be absent, which means the mirror was written
// under the issuance's epoch 0 key.
[[nodiscard]] bool
checkMirrorEpochs(
Account const& holder,
std::optional<std::uint32_t> issuerKeyMirrorEpoch,
std::optional<std::uint32_t> auditorKeyMirrorEpoch) 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.
@@ -678,20 +690,31 @@ public:
operator Asset() const;
void
// Generates the account's next key pair and returns the key epoch it landed
// at, leaving the earlier ones retrievable.
std::uint32_t
generateKeyPair(Account const& account);
// Returns the account's public key at the given key epoch, or its latest key when
// no epoch is given.
[[nodiscard]] std::optional<Buffer>
getPubKey(Account const& account) const;
getPubKey(Account const& account, std::optional<std::uint32_t> epoch = std::nullopt) const;
// Returns the account's private key at the given key epoch, or its latest key when
// no epoch is given.
[[nodiscard]] std::optional<Buffer>
getPrivKey(Account const& account) const;
getPrivKey(Account const& account, std::optional<std::uint32_t> epoch = std::nullopt) const;
[[nodiscard]] Buffer
encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor) const;
// Decrypts with the account's key at the given key epoch, or its latest key
// when no epoch is given.
[[nodiscard]] std::optional<uint64_t>
decryptAmount(Account const& account, Buffer const& amt) const;
decryptAmount(
Account const& account,
Buffer const& amt,
std::optional<std::uint32_t> epoch = std::nullopt) const;
[[nodiscard]] std::optional<uint64_t>
getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const;
@@ -745,6 +768,10 @@ private:
std::function<bool(SLEP const& sle)> const& cb,
std::optional<Account> const& holder = std::nullopt) const;
// Reads one of the holder's mirror key epochs off their MPToken.
[[nodiscard]] std::optional<std::uint32_t>
getMirrorEpoch(Account const& holder, SF_UINT32 const& field) const;
template <typename A>
TER
submit(A const& arg, json::Value jv)

View File

@@ -0,0 +1,83 @@
#include <test/jtx/Env.h>
#include <test/jtx/envconfig.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/overlay/Overlay.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/peerfinder/detail/Tuning.h>
#include <utility>
namespace xrpl::test {
using namespace jtx;
/**
* Tests for `Overlay::limit()`, the configured peer allowance reported once
* `OverlayImpl::start()` applies the computed `peer_finder::Config`.
*
* `ApplicationImp::fdRequired()` runs before `OverlayImpl::start()` does, so it
* always sees the peer finder manager's default-constructed configuration and
* never this value; `Overlay::limit()` instead surfaces through the PeerFinder
* property stream and other post-startup callers.
*
* `jtx::Env` runs standalone, and `ServerHandler` strips the `peer` protocol
* from every configured port under `config.standalone()`, so the peer port
* declared here is never bound and incoming connections are disabled
* throughout; every limit in this suite is an outbound-only allowance. The
* inbound cases live alongside `peer_finder::Config::makeConfig`, which takes
* the port as a parameter.
*/
class OverlayLimit_test : public beast::unit_test::Suite
{
void
testLegacyPeersMax()
{
testcase("Legacy peers_max is reported");
auto config = jtx::envconfig();
config->peersMax = 40;
Env env(*this, std::move(config));
BEAST_EXPECT(env.app().getOverlay().limit() == 40);
}
void
testPerDirectionPeerLimits()
{
testcase("Per-direction peer limits are reported");
// With incoming connections disabled the 50 inbound slots are dropped
// and only the outbound allowance remains, so neither zero (the value
// `maxPeers` used to hold in this branch of makeConfig) nor 70 (the
// unconditional sum of both directions) is correct.
auto config = jtx::envconfig();
config->peersInMax = 50;
config->peersOutMax = 20;
Env env(*this, std::move(config));
BEAST_EXPECT(env.app().getOverlay().limit() == 20);
}
void
testDefaultConfig()
{
testcase("A default configuration reports the default limit");
Env env(*this);
BEAST_EXPECT(env.app().getOverlay().limit() == peer_finder::tuning::kDefaultMaxPeers);
}
void
run() override
{
testLegacyPeersMax();
testPerDirectionPeerLimits();
testDefaultConfig();
}
};
BEAST_DEFINE_TESTSUITE(OverlayLimit, overlay, xrpl);
} // namespace xrpl::test

View File

@@ -126,9 +126,16 @@ class NoRippleCheck_test : public beast::unit_test::Suite
params[jss::account] = toBase58(TokenType::NodePrivate, alice.sk());
params[jss::role] = "user";
params[jss::ledger] = "current";
params[jss::transactions] = true;
auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
BEAST_EXPECT(result[jss::error] == "actMalformed");
BEAST_EXPECT(result[jss::error_message] == "Account malformed.");
// The changelog promises malformed-account responses carry
// neither `transactions` nor any ledger metadata.
BEAST_EXPECT(!result.isMember(jss::transactions));
BEAST_EXPECT(!result.isMember(jss::ledger_hash));
BEAST_EXPECT(!result.isMember(jss::ledger_index));
BEAST_EXPECT(!result.isMember(jss::validated));
}
{
@@ -194,6 +201,7 @@ class NoRippleCheck_test : public beast::unit_test::Suite
if (!BEAST_EXPECT(pa.isArray()))
return;
BEAST_EXPECT(!result.isMember(jss::transactions));
if (problems)
{
if (!BEAST_EXPECT(pa.size() == 2))
@@ -219,12 +227,12 @@ class NoRippleCheck_test : public beast::unit_test::Suite
// time.
params[jss::transactions] = true;
result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
if (!BEAST_EXPECT(result[jss::transactions].isArray()))
return;
auto const txs = result[jss::transactions];
if (problems)
{
if (!BEAST_EXPECT(result[jss::transactions].isArray()))
return;
if (!BEAST_EXPECT(txs.size() == (user ? 1 : 2)))
return;

View File

@@ -1214,6 +1214,9 @@ TEST(PeerFinderConfig, applies_legacy_and_explicit_peer_limits)
.expectedOut = 10,
.expectedIn = 0,
.expectedIpLimit = 1},
// A port of zero disables incoming connections, so the configured
// inbound limit is dropped and the per-IP inbound limit collapses to
// one, exactly as in the legacy private case above.
{.name = "new in 100/out 10, private",
.maxPeers = {},
.maxIn = 100,
@@ -1221,7 +1224,7 @@ TEST(PeerFinderConfig, applies_legacy_and_explicit_peer_limits)
.port = 0,
.expectedOut = 10,
.expectedIn = 0,
.expectedIpLimit = 6}};
.expectedIpLimit = 1}};
for (auto const& testCase : cases)
{
@@ -1239,6 +1242,13 @@ TEST(PeerFinderConfig, applies_legacy_and_explicit_peer_limits)
EXPECT_EQ(counts.inMax(), testCase.expectedIn);
EXPECT_EQ(config.ipLimit, testCase.expectedIpLimit);
// The configuration itself carries the same per-direction allowances
// that the slot counts derive, and `maxPeers` is their total. Callers
// such as `Overlay::limit` read `maxPeers` directly.
EXPECT_EQ(config.outPeers, testCase.expectedOut);
EXPECT_EQ(config.inPeers, testCase.expectedIn);
EXPECT_EQ(config.maxPeers, config.inPeers + config.outPeers);
NiceMock<MockStore> store;
allowEmptyStore(store);
NiceMock<MockChecker> checker;

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

@@ -32,6 +32,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
auto const confidentialBalanceVersionValue = canonical_UINT32();
auto const issuerEncryptedBalanceValue = canonical_VL();
auto const auditorEncryptedBalanceValue = canonical_VL();
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
auto const holderEncryptionKeyValue = canonical_VL();
MPTokenBuilder builder{
@@ -49,6 +51,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
builder.setConfidentialBalanceVersion(confidentialBalanceVersionValue);
builder.setIssuerEncryptedBalance(issuerEncryptedBalanceValue);
builder.setAuditorEncryptedBalance(auditorEncryptedBalanceValue);
builder.setIssuerKeyMirrorEpoch(issuerKeyMirrorEpochValue);
builder.setAuditorKeyMirrorEpoch(auditorKeyMirrorEpochValue);
builder.setHolderEncryptionKey(holderEncryptionKeyValue);
builder.setLedgerIndex(index);
@@ -146,6 +150,22 @@ TEST(MPTokenTests, BuilderSettersRoundTrip)
EXPECT_TRUE(entry.hasAuditorEncryptedBalance());
}
{
auto const& expected = issuerKeyMirrorEpochValue;
auto const actualOpt = entry.getIssuerKeyMirrorEpoch();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfIssuerKeyMirrorEpoch");
EXPECT_TRUE(entry.hasIssuerKeyMirrorEpoch());
}
{
auto const& expected = auditorKeyMirrorEpochValue;
auto const actualOpt = entry.getAuditorKeyMirrorEpoch();
ASSERT_TRUE(actualOpt.has_value());
expectEqualField(expected, *actualOpt, "sfAuditorKeyMirrorEpoch");
EXPECT_TRUE(entry.hasAuditorKeyMirrorEpoch());
}
{
auto const& expected = holderEncryptionKeyValue;
auto const actualOpt = entry.getHolderEncryptionKey();
@@ -179,6 +199,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
auto const confidentialBalanceVersionValue = canonical_UINT32();
auto const issuerEncryptedBalanceValue = canonical_VL();
auto const auditorEncryptedBalanceValue = canonical_VL();
auto const issuerKeyMirrorEpochValue = canonical_UINT32();
auto const auditorKeyMirrorEpochValue = canonical_UINT32();
auto const holderEncryptionKeyValue = canonical_VL();
auto sle = std::make_shared<SLE>(MPToken::entryType, index);
@@ -195,6 +217,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
sle->at(sfConfidentialBalanceVersion) = confidentialBalanceVersionValue;
sle->at(sfIssuerEncryptedBalance) = issuerEncryptedBalanceValue;
sle->at(sfAuditorEncryptedBalance) = auditorEncryptedBalanceValue;
sle->at(sfIssuerKeyMirrorEpoch) = issuerKeyMirrorEpochValue;
sle->at(sfAuditorKeyMirrorEpoch) = auditorKeyMirrorEpochValue;
sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue;
MPTokenBuilder builderFromSle{sle};
@@ -347,6 +371,32 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip)
expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptedBalance");
}
{
auto const& expected = issuerKeyMirrorEpochValue;
auto const fromSleOpt = entryFromSle.getIssuerKeyMirrorEpoch();
auto const fromBuilderOpt = entryFromBuilder.getIssuerKeyMirrorEpoch();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfIssuerKeyMirrorEpoch");
expectEqualField(expected, *fromBuilderOpt, "sfIssuerKeyMirrorEpoch");
}
{
auto const& expected = auditorKeyMirrorEpochValue;
auto const fromSleOpt = entryFromSle.getAuditorKeyMirrorEpoch();
auto const fromBuilderOpt = entryFromBuilder.getAuditorKeyMirrorEpoch();
ASSERT_TRUE(fromSleOpt.has_value());
ASSERT_TRUE(fromBuilderOpt.has_value());
expectEqualField(expected, *fromSleOpt, "sfAuditorKeyMirrorEpoch");
expectEqualField(expected, *fromBuilderOpt, "sfAuditorKeyMirrorEpoch");
}
{
auto const& expected = holderEncryptionKeyValue;
@@ -436,6 +486,10 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt)
EXPECT_FALSE(entry.getIssuerEncryptedBalance().has_value());
EXPECT_FALSE(entry.hasAuditorEncryptedBalance());
EXPECT_FALSE(entry.getAuditorEncryptedBalance().has_value());
EXPECT_FALSE(entry.hasIssuerKeyMirrorEpoch());
EXPECT_FALSE(entry.getIssuerKeyMirrorEpoch().has_value());
EXPECT_FALSE(entry.hasAuditorKeyMirrorEpoch());
EXPECT_FALSE(entry.getAuditorKeyMirrorEpoch().has_value());
EXPECT_FALSE(entry.hasHolderEncryptionKey());
EXPECT_FALSE(entry.getHolderEncryptionKey().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

@@ -21,6 +21,8 @@
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
namespace xrpl {
@@ -32,12 +34,13 @@ fillTransaction(
std::uint32_t& sequence,
ReadView const& ledger)
{
txArray["Sequence"] = json::UInt(sequence++);
txArray["Account"] = toBase58(accountID);
txArray[jss::Sequence] = json::UInt(sequence++);
txArray[jss::Account] = toBase58(accountID);
auto& fees = ledger.fees();
// Convert the reference transaction cost in fee units to drops
// scaled to represent the current fee load.
txArray["Fee"] = scaleFeeLoad(fees.base, context.app.getFeeTrack(), fees, false).jsonClipped();
txArray[jss::Fee] =
scaleFeeLoad(fees.base, context.app.getFeeTrack(), fees, false).jsonClipped();
}
// {
@@ -53,24 +56,33 @@ doNoRippleCheck(rpc::JsonContext& context)
{
auto const& params(context.params);
if (!params.isMember(jss::account))
return rpc::missingFieldError("account");
if (!params.isMember("role"))
return rpc::missingFieldError("role");
return rpc::missingFieldError(jss::account);
if (!params[jss::account].isString())
return rpc::invalidFieldError(jss::account);
auto id = parseBase58<AccountID>(params[jss::account].asString());
if (!id)
{
return rpcError(RpcActMalformed);
}
auto const accountID{id.value()};
if (!params.isMember(jss::role))
return rpc::missingFieldError(jss::role);
bool roleGateway = false;
{
std::string const role = params["role"].asString();
if (role == "gateway")
if (!params[jss::role].isString())
return rpc::expectedFieldError(jss::role, "string");
std::string const role = params[jss::role].asString();
if (role == jss::gateway)
{
roleGateway = true;
}
else if (role != "user")
else if (role != jss::user)
{
return rpc::invalidFieldError("role");
return rpc::invalidFieldError(jss::role);
}
}
@@ -78,61 +90,49 @@ doNoRippleCheck(rpc::JsonContext& context)
if (auto err = readLimitField(limit, rpc::tuning::kNoRippleCheck, context))
return *err;
bool transactions = false;
if (params.isMember(jss::transactions))
transactions = params["transactions"].asBool();
// The document[https://xrpl.org/noripple_check.html#noripple_check] states
// that transactions params is a boolean value, however, assigning any
// string value works. Do not allow this. This check is for api Version 2
// onwards only
// API v1 silently accepts any string as `transactions`; v2+ enforces bool.
if (context.apiVersion > 1u && params.isMember(jss::transactions) &&
!params[jss::transactions].isBool())
{
return rpc::invalidFieldError(jss::transactions);
}
bool transactions = false;
if (params.isMember(jss::transactions))
transactions = params[jss::transactions].asBool();
std::shared_ptr<ReadView const> ledger;
auto result = rpc::lookupLedger(ledger, context);
if (!ledger)
return result;
json::Value dummy; // NOLINT(misc-const-correctness)
json::Value& jvTransactions =
transactions ? (result[jss::transactions] = json::ValueType::Array) : dummy;
auto id = parseBase58<AccountID>(params[jss::account].asString());
if (!id)
{
rpc::injectError(RpcActMalformed, result);
return result;
}
auto const accountID{id.value()};
auto const sle = ledger->read(keylet::account(accountID));
if (!sle)
return rpcError(RpcActNotFound);
std::uint32_t seq = sle->getFieldU32(sfSequence);
json::Value& problems = (result["problems"] = json::ValueType::Array);
json::Value& problems = (result[jss::problems] = json::ValueType::Array);
bool const bDefaultRipple = sle->isFlag(lsfDefaultRipple);
bool const defaultRipple = sle->isFlag(lsfDefaultRipple);
if (bDefaultRipple && !roleGateway)
json::Value jvTransactions = json::ValueType::Array;
if (defaultRipple && !roleGateway)
{
problems.append(
"You appear to have set your default ripple flag even though you "
"are not a gateway. This is not recommended unless you are "
"experimenting");
}
else if (roleGateway && !bDefaultRipple)
else if (roleGateway && !defaultRipple)
{
problems.append("You should immediately set your default ripple flag");
if (transactions)
{
json::Value& tx = jvTransactions.append(json::ValueType::Object);
tx["TransactionType"] = jss::AccountSet;
tx["SetFlag"] = 8;
tx[jss::TransactionType] = jss::AccountSet;
tx[jss::SetFlag] = 8;
fillTransaction(context, tx, accountID, seq, *ledger);
}
}
@@ -140,18 +140,18 @@ doNoRippleCheck(rpc::JsonContext& context)
forEachItemAfter(*ledger, accountID, uint256(), 0, limit, [&](SLE::const_ref ownedItem) {
if (ownedItem->getType() == ltRIPPLE_STATE)
{
bool const bLow = accountID == ownedItem->getFieldAmount(sfLowLimit).getIssuer();
bool const low = accountID == ownedItem->getFieldAmount(sfLowLimit).getIssuer();
bool const bNoRipple = ownedItem->isFlag(bLow ? lsfLowNoRipple : lsfHighNoRipple);
bool const noRipple = ownedItem->isFlag(low ? lsfLowNoRipple : lsfHighNoRipple);
std::string problem;
bool needFix = false;
if (bNoRipple && roleGateway)
if (noRipple && roleGateway)
{
problem = "You should clear the no ripple flag on your ";
needFix = true;
}
else if (!roleGateway && !bNoRipple)
else if (!roleGateway && !noRipple)
{
problem = "You should probably set the no ripple flag on your ";
needFix = true;
@@ -159,22 +159,25 @@ doNoRippleCheck(rpc::JsonContext& context)
if (needFix)
{
AccountID const peer =
ownedItem->getFieldAmount(bLow ? sfHighLimit : sfLowLimit).getIssuer();
ownedItem->getFieldAmount(low ? sfHighLimit : sfLowLimit).getIssuer();
STAmount const peerLimit =
ownedItem->getFieldAmount(bLow ? sfHighLimit : sfLowLimit);
ownedItem->getFieldAmount(low ? sfHighLimit : sfLowLimit);
problem += to_string(peerLimit.get<Issue>().currency);
problem += " line to ";
problem += to_string(peerLimit.getIssuer());
problems.append(problem);
STAmount limitAmount(ownedItem->getFieldAmount(bLow ? sfLowLimit : sfHighLimit));
STAmount limitAmount(ownedItem->getFieldAmount(low ? sfLowLimit : sfHighLimit));
limitAmount.get<Issue>().account = peer;
json::Value& tx = jvTransactions.append(json::ValueType::Object);
tx["TransactionType"] = jss::TrustSet;
tx["LimitAmount"] = limitAmount.getJson(JsonOptions::Values::None);
tx["Flags"] = bNoRipple ? tfClearNoRipple : tfSetNoRipple;
fillTransaction(context, tx, accountID, seq, *ledger);
if (transactions)
{
json::Value& tx = jvTransactions.append(json::ValueType::Object);
tx[jss::TransactionType] = jss::TrustSet;
tx[jss::LimitAmount] = limitAmount.getJson(JsonOptions::Values::None);
tx[jss::Flags] = noRipple ? tfClearNoRipple : tfSetNoRipple;
fillTransaction(context, tx, accountID, seq, *ledger);
}
return true;
}
@@ -182,6 +185,8 @@ doNoRippleCheck(rpc::JsonContext& context)
return false;
});
if (transactions)
result[jss::transactions] = std::move(jvTransactions);
return result;
}