Compare commits

...

44 Commits

Author SHA1 Message Date
Ed Hennis
339a2d30f7 Merge remote-tracking branch 'XRPLF/develop' into ximinez/313-tests
* XRPLF/develop:
  chore: Run clang_tidy_check with `pass_filenames: false` from pre-commit (7800)
  feat: Add delegate filter param for account_tx RPC (6126)
  refactor: Move `jss.h` `include` out of `Indexes.h` (7799)
  test: Add tests for check doxygen style (7795)
  style: Add pre-commit hook to check doxygen style (7794)
  style: Unify style for all Doxygen comments (7776)
  fix: Improve Number addition/subtraction rounding (7369)
  feat: XLS-68: Sponsor, 5887 continuation  (7350)
2026-07-13 20:43:05 -04:00
Ayaz Salikhov
cd06ee221d chore: Run clang_tidy_check with pass_filenames: false from pre-commit (#7800) 2026-07-13 19:19:18 +00:00
Peter Chen
752dab8b30 feat: Add delegate filter param for account_tx RPC (#6126) 2026-07-13 18:44:59 +00:00
Mayukha Vadari
86583bc34e refactor: Move jss.h include out of Indexes.h (#7799) 2026-07-13 17:44:47 +00:00
Ed Hennis
188c044f34 Fix syntax error 2026-07-10 11:48:17 -04:00
Ed Hennis
654c9334f4 Update src/tests/libxrpl/basics/base_uint_test.cpp
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2026-07-10 11:36:03 -04:00
Ed Hennis
ee7bfe016f Refactor the base_uint death tests into lambdas
- Hoping this will fix the Windows build
2026-07-09 21:22:30 -04:00
Ed Hennis
fc29cc227d Punt debug death tests on VOIDSTAR builds 2026-07-09 19:41:09 -04:00
Ed Hennis
b2e6ee18da Merge branch 'develop' into ximinez/313-tests 2026-07-08 17:23:37 -04:00
Ed Hennis
3555812b68 Merge branch 'develop' into ximinez/313-tests 2026-07-07 19:32:48 -04:00
Ed Hennis
203d7d6bf8 Address AI review feedback, and fix AI screwups
- Pad base_uint::operator= with 0s.
- Add more base_uint tests for operator=, and simplify the "death" error
  match.
- Put back some } that AI suggestions removed, and I didn't notice when
  I took the suggestions..
2026-07-07 19:28:44 -04:00
Ed Hennis
2a30d519b9 Apply suggestions from code review
Make test result checks more specific.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-07 17:39:04 -04:00
Ed Hennis
7f0dd14235 fixup previous merge: name and expected failure message 2026-07-07 17:33:53 -04:00
Ed Hennis
ea64e9f869 Merge remote-tracking branch 'XRPLF/develop' into ximinez/313-tests
* XRPLF/develop:
  chore: Enable most cppcoreguidelines checks (7660)
  test: Migrate basics Beast tests to GTest (7136)
  test: Migrate resource, shamap Beast tests to GTest (7133)
2026-07-07 17:25:43 -04:00
Ed Hennis
56c4315834 Merge branch 'develop' into ximinez/313-tests 2026-07-06 13:33:03 -04:00
Ed Hennis
e57be78e50 Merge remote-tracking branch 'XRPLF/develop' into ximinez/313-tests
* XRPLF/develop:
  chore: Make clang-tidy happy on macOS (7701)
2026-07-02 13:42:05 -04:00
Ed Hennis
8d5834d06b Merge remote-tracking branch 'XRPLF/develop' into ximinez/313-tests
* XRPLF/develop:
  chore: Improve pre-commit hooks (7702)
2026-07-02 13:29:38 -04:00
Ed Hennis
140c534613 Remove NDEBUGs 2026-07-02 13:23:45 -04:00
Ed Hennis
0e518b6fba Merge branch 'develop' into ximinez/313-tests 2026-07-02 11:18:40 -04:00
Ed Hennis
241ba86467 Remove duplicate 2026-07-01 14:21:59 -04:00
Ed Hennis
4bb15b91c2 Revert "Remove the _other_ test case"
This reverts commit a16d15660e.
2026-07-01 13:45:00 -04:00
Ed Hennis
03520f04d1 Use the smaller buffer size 2026-07-01 13:44:40 -04:00
Ed Hennis
538f32728f Merge branch 'develop' into ximinez/313-tests 2026-07-01 13:23:21 -04:00
Ed Hennis
0999af81dc Merge branch 'develop' into ximinez/313-tests 2026-07-01 12:16:02 -04:00
Ed Hennis
e9973cf0c4 Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-30 23:02:13 -04:00
Ed Hennis
3f511e8067 Merge branch 'develop' into ximinez/313-tests 2026-06-30 16:28:12 -04:00
Ed Hennis
65691f7ef1 AI: Fix failure output in src/test/basics/base_uint_test.cpp
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-06-25 12:32:08 -04:00
Ed Hennis
3268c63f1a Merge branch 'develop' into ximinez/313-tests 2026-06-25 11:56:00 -04:00
Ed Hennis
7c2a1297db clang-tidy: const correctness 2026-06-25 11:54:49 -04:00
Ed Hennis
a16d15660e Remove the _other_ test case 2026-06-24 17:05:40 -04:00
Ed Hennis
fd406e2500 Revert "Remove test case"
This reverts commit 73dca1111a.
2026-06-24 16:15:34 -04:00
Ed Hennis
73dca1111a Remove test case 2026-06-23 18:55:05 -04:00
Ed Hennis
923a822491 Merge remote-tracking branch 'XRPLF/develop' into ximinez/313-tests
* XRPLF/develop:
  refactor: Clean up tec object deletion logic (6588)
  fix: Move AMMInvariant weakInvariantCheck logic into the transaction (7032)
  fix: Improve ValidAMM invariant (7295)
  feat: Remove clear mutable flags for DynamicMPT XLS-94 (7439)
  ci: Build and push docker images in forks too (7588)
  ci: [DEPENDABOT] bump actions/checkout from 6.0.3 to 7.0.0 (7585)
  fix: Ensure xrpld service directories exist at startup (7565)
  fix: Use template for granular delegation permissions (6613)
2026-06-22 18:15:16 -04:00
Ed Hennis
9ab4f325ce test: Check some anomalous results in CI 2026-06-22 18:13:46 -04:00
Ed Hennis
05ce347036 Explicitly initialize base_uint.data_ 2026-06-22 18:13:45 -04:00
Ed Hennis
f00c07da2f Debug the testFromRawSizeMismatch test in CI 2026-06-22 18:13:45 -04:00
Ed Hennis
850dba46fb Update levelization 2026-06-22 18:13:45 -04:00
Ed Hennis
2853c64974 clang-tidy: includes and variable names 2026-06-22 18:13:44 -04:00
Ed Hennis
dd95b326b3 Update names, etc. to match develop 2026-06-22 18:13:44 -04:00
Ed Hennis
f2a22d4b99 Fix formatting 2026-06-22 18:13:44 -04:00
Ed Hennis
0fc1c735a1 Add back tests introduced by #29 #57 2026-06-22 18:13:44 -04:00
Ed Hennis
ea7a7de5de Add back tests introduced by #64 2026-06-22 18:13:43 -04:00
Ed Hennis
62bce4e178 test: Add tests for #46 2026-06-22 18:13:43 -04:00
Sergey Kuznetsov
3086504658 tests: Tests for fix in account objects rpc (#58) (#59) 2026-06-22 18:13:43 -04:00
20 changed files with 1307 additions and 49 deletions

View File

@@ -65,6 +65,7 @@ words:
- Btrfs
- Buildx
- canonicality
- canonicalised
- changespq
- checkme
- choco

View File

@@ -159,6 +159,7 @@ test.peerfinder > xrpl.protocol
test.protocol > test.jtx
test.protocol > test.unit_test
test.protocol > xrpl.basics
test.protocol > xrpld.core
test.protocol > xrpl.json
test.protocol > xrpl.protocol
test.rpc > test.jtx

View File

@@ -32,6 +32,11 @@ repos:
# as standalone translation units, so they have no compile_commands.json
# entry to lint (verify_headers checks them transitively).
exclude: '^include/xrpl/protocol_autogen|\.ipp$'
# run-clang-tidy --fix may edit headers included by files it is not run on,
# so pre-commit must not split the files across parallel hook invocations.
# The script determines the staged files itself and lets run-clang-tidy
# handle parallelism internally.
pass_filenames: false
- id: fix-include-style
name: fix include style
entry: ./bin/pre-commit/fix_include_style.py

View File

@@ -28,6 +28,9 @@ This section contains changes targeting a future version.
### Additions
- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`.
When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present.
- `ledger_entry`, `account_objects`: The `Delegate` ledger entry now includes an optional `DestinationNode` field, which stores the index into the authorized account's owner directory. This field is present on entries created after bidirectional directory tracking was introduced and may appear in RPC responses for those entries. ([#6681](https://github.com/XRPLF/rippled/pull/6681))
- `server_definitions`: Added the following new sections to the response ([#6321](https://github.com/XRPLF/rippled/pull/6321)):

View File

@@ -1,27 +1,46 @@
#!/usr/bin/env python3
"""Pre-commit hook that runs clang-tidy on changed files using run-clang-tidy.
"""Pre-commit hook that runs clang-tidy on staged files using run-clang-tidy.
The set of files is chosen by pre-commit (see .pre-commit-config.yaml), which
filters to C/C++ sources and excludes `.ipp` fragments. Headers are linted
directly: the `verify_headers` build option (ON by default) compiles every
`.h`/`.hpp` on its own, so each header is the main file of its own
compile_commands.json entry and run-clang-tidy can analyse it just like a
`.cpp`.
The script determines the staged files itself (see `pass_filenames: false` in
.pre-commit-config.yaml) so run-clang-tidy is run once and handles parallelism
internally: pre-commit would otherwise split the files across parallel hook
invocations that race when fixes edit a shared header.
Fixes are collected with `-export-fixes` and applied by clang-apply-replacements
in a separate step rather than with run-clang-tidy's `-fix`. The `add_module`
build isolates each module's headers behind a per-module symlink directory
(build/modules/<module>/...), so a header reachable from several translation
units is referenced through different paths that all resolve to the same source
file. clang-apply-replacements deduplicates identical replacements by their
literal path, so those paths must be canonicalised to the real source path
first; otherwise the same fix is applied once per path and corrupts the header.
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
CLANG_TIDY_VERSION = 22
# Extensions run-clang-tidy can analyse: `.cpp` translation units and, thanks to
# the `verify_headers` build option, `.h`/`.hpp` headers (each has its own
# compile_commands.json entry). `.ipp` fragments have no entry and are skipped.
TIDY_EXTENSIONS = {".cpp", ".h", ".hpp"}
def find_run_clang_tidy() -> str | None:
for candidate in (f"run-clang-tidy-{CLANG_TIDY_VERSION}", "run-clang-tidy"):
# A single-quoted `FilePath:` entry in an -export-fixes YAML file, allowing the
# `- ` marker that precedes it inside a `Replacements:` sequence. clang-tidy
# emits paths single-quoted and doubles any embedded quote per YAML rules.
FILEPATH_RE = re.compile(r"^(\s*(?:-\s+)?FilePath:\s*)'((?:[^']|'')*)'\s*$")
def find_tool(name: str) -> str | None:
for candidate in (f"{name}-{CLANG_TIDY_VERSION}", name):
if path := shutil.which(candidate):
return path
return None
@@ -35,23 +54,43 @@ def find_build_dir(repo_root: Path) -> Path | None:
return None
def staged_files(repo_root: Path) -> list[Path]:
"""Return absolute paths of staged, lint-able C/C++ files.
`--diff-filter=d` excludes deletions so we never lint a removed file.
"""
output = subprocess.check_output(
["git", "diff", "--staged", "--name-only", "--diff-filter=d", "--"]
+ [f"*{ext}" for ext in TIDY_EXTENSIONS],
text=True,
cwd=repo_root,
)
return [repo_root / rel for rel in output.splitlines() if rel]
def canonicalize_fix_paths(fixes_dir: Path) -> None:
"""Rewrite every `FilePath` in the exported fixes to its real source path.
A header included through a module's isolation symlink is recorded under that
symlink's path; collapsing all paths to the same real file lets
clang-apply-replacements recognise the per-translation-unit duplicates and
apply each fix once.
"""
for yaml in fixes_dir.glob("*.yaml"):
lines = []
for line in yaml.read_text().splitlines():
if m := FILEPATH_RE.match(line):
path = m.group(2).replace("''", "'")
real = os.path.realpath(path).replace("'", "''")
line = f"{m.group(1)}'{real}'"
lines.append(line)
yaml.write_text("\n".join(lines) + "\n")
def main():
if not os.environ.get("TIDY"):
return 0
files = sys.argv[1:]
if not files:
return 0
run_clang_tidy = find_run_clang_tidy()
if not run_clang_tidy:
print(
f"clang-tidy check failed: TIDY is enabled but neither "
f"'run-clang-tidy-{CLANG_TIDY_VERSION}' nor 'run-clang-tidy' was found in PATH.",
file=sys.stderr,
)
return 1
repo_root = Path(
subprocess.check_output(
["git", "rev-parse", "--show-toplevel"],
@@ -59,6 +98,29 @@ def main():
text=True,
).strip()
)
files = staged_files(repo_root)
if not files:
return 0
run_clang_tidy = find_tool("run-clang-tidy")
clang_apply_replacements = find_tool("clang-apply-replacements")
missing = [
name
for name, path in (
("run-clang-tidy", run_clang_tidy),
("clang-apply-replacements", clang_apply_replacements),
)
if not path
]
if missing:
print(
f"clang-tidy check failed: TIDY is enabled but {' and '.join(missing)} "
f"was not found in PATH (tried the '-{CLANG_TIDY_VERSION}' suffix too).",
file=sys.stderr,
)
return 1
build_dir = find_build_dir(repo_root)
if not build_dir:
print(
@@ -68,11 +130,23 @@ def main():
)
return 1
result = subprocess.run(
[run_clang_tidy, "-quiet", "-p", str(build_dir), "-fix", "-allow-no-checks"]
+ files
)
return result.returncode
with tempfile.TemporaryDirectory() as fixes_dir:
result = subprocess.run(
[
run_clang_tidy,
"-quiet",
"-p",
build_dir,
"-export-fixes",
fixes_dir,
"-allow-no-checks",
]
+ files
)
canonicalize_fix_paths(Path(fixes_dir))
applied = subprocess.run([clang_apply_replacements, fixes_dir])
return result.returncode or applied.returncode
if __name__ == "__main__":

View File

@@ -308,7 +308,9 @@ public:
XRPL_ASSERT(
c.size() * sizeof(typename Container::value_type) == size(),
"xrpl::BaseUInt::fromRaw(Container auto) : input size match");
std::memcpy(result.data_.data(), c.data(), size());
std::size_t const canCopy =
std::min(size(), c.size() * sizeof(typename Container::value_type));
std::memcpy(result.data_.data(), c.data(), canCopy);
return result;
}
@@ -322,7 +324,11 @@ public:
XRPL_ASSERT(
c.size() * sizeof(typename Container::value_type) == size(),
"xrpl::BaseUInt::operator=(Container auto) : input size match");
std::memcpy(data_.data(), c.data(), size());
std::size_t const canCopy =
std::min(size(), c.size() * sizeof(typename Container::value_type));
if (canCopy < size())
*this = beast::kZero;
std::memcpy(data_.data(), c.data(), canCopy);
return *this;
}

View File

@@ -13,7 +13,6 @@
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STXChainBridge.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <array>
#include <cstdint>
@@ -423,21 +422,8 @@ struct KeyletDesc
bool includeInTests{};
};
// This list should include all of the keylet functions that take a single
// AccountID parameter.
std::array<KeyletDesc<AccountID const&>, 6> const kDirectAccountKeylets{
{{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false},
{.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true},
{.function = &keylet::signerList, .expectedLEName = jss::SignerList, .includeInTests = true},
// It's normally impossible to create an item at nftpage_min, but
// test it anyway, since the invariant checks for it.
{.function = &keylet::nftokenPageMin,
.expectedLEName = jss::NFTokenPage,
.includeInTests = true},
{.function = &keylet::nftokenPageMax,
.expectedLEName = jss::NFTokenPage,
.includeInTests = true},
{.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}};
// This list should include all of the keylet functions that take a single AccountID parameter.
extern std::array<KeyletDesc<AccountID const&>, 6> const kDirectAccountKeylets;
MPTID
makeMptID(std::uint32_t sequence, AccountID const& account);

View File

@@ -110,6 +110,7 @@ JSS(accounts); // in: LedgerEntry, Subscribe, handlers/Ledger
JSS(accounts_proposed); // in: Subscribe, Unsubscribe
JSS(action); //
JSS(active); // out: OverlayImpl
JSS(actor); // in/out: AccountTx
JSS(acquiring); // out: LedgerRequest
JSS(address); // out: PeerImp
JSS(affected); // out: AcceptedLedgerTx
@@ -133,6 +134,7 @@ JSS(attestation_reward_account); //
JSS(auction_slot); // out: amm_info
JSS(authorized); // out: AccountLines
JSS(authorize); // out: delegate
JSS(authorizer); // in/out: AccountTx
JSS(authorized_credentials); // in: ledger_entry DepositPreauth
JSS(auth_accounts); // out: amm_info
JSS(auth_change); // out: AccountInfo
@@ -191,6 +193,7 @@ JSS(converge_time); // out: NetworkOPs
JSS(converge_time_s); // out: NetworkOPs
JSS(cookie); // out: NetworkOPs
JSS(count); // in: AccountTx*, ValidatorList
JSS(counter_party); // in/out: AccountTx
JSS(counters); // in/out: retrieve counters
JSS(credentials); // in: deposit_authorized
JSS(credential_type); // in: LedgerEntry DepositPreauth
@@ -270,6 +273,7 @@ JSS(freeze); // out: AccountLines
JSS(freeze_peer); // out: AccountLines
JSS(deep_freeze); // out: AccountLines
JSS(deep_freeze_peer); // out: AccountLines
JSS(delegate_filter); // in/out: AccountTx
JSS(frozen_balances); // out: GatewayBalances
JSS(full); // in: LedgerClearer, handlers/Ledger
JSS(full_reply); // out: PathFind

View File

@@ -46,6 +46,22 @@ struct LedgerRange
uint32_t max;
};
/**
* @brief Enumeration of possible delegate types that can occur during filtering in account_tx
*/
enum class DelegateType {
Actor, ///< Another account signed and submitted transactions on behalf of this account (this
///< account is the owner/delegator).
Authorizer ///< This account signed and submitted transactions on behalf of another account
///< (this account is the signer/delegatee).
};
struct DelegateFilter
{
DelegateType type = DelegateType::Actor;
std::optional<AccountID> counterparty;
};
class RelationalDatabase
{
public:
@@ -82,6 +98,7 @@ public:
std::optional<AccountTxMarker> marker;
std::uint32_t limit = 0;
bool bAdmin = false;
std::optional<DelegateFilter> delegate;
};
using AccountTx = std::pair<std::shared_ptr<Transaction>, std::shared_ptr<TxMeta>>;
@@ -101,6 +118,7 @@ public:
bool forward = false;
uint32_t limit = 0;
std::optional<AccountTxMarker> marker;
std::optional<DelegateFilter> delegate;
};
struct AccountTxResult
@@ -109,6 +127,7 @@ public:
LedgerRange ledgerRange{};
uint32_t limit = 0;
std::optional<AccountTxMarker> marker;
std::optional<DelegateFilter> delegate;
};
virtual ~RelationalDatabase() = default;

View File

@@ -29,7 +29,7 @@ src:test/beast/beast_PropertyStream_test.cpp
src:src/test/app/Invariants_test.cpp
# ASan false positive: stack-use-after-scope in ErrorCodes.h inline functions.
# When Clang inlines the StaticString overloads (e.g. invalid_field_error(StaticString)),
# When Clang inlines the StaticString overloads (e.g. invalidFieldError(StaticString)),
# ASan scope-poisons the temporary std::string before the inlined callee finishes reading
# through the const ref. This corrupts the coroutine stack and crashes the Simulate test.
# See asan.supp comments for full explanation and planned fix.

View File

@@ -17,6 +17,7 @@
#include <xrpl/protocol/SeqProxy.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/nftPageMask.h>
#include <boost/endian/conversion.hpp>
@@ -32,6 +33,23 @@
namespace xrpl {
// This list should include all of the keylet functions that take a single
// AccountID parameter. Declared in Indexes.h; defined here so the header need
// not include jss.h.
std::array<KeyletDesc<AccountID const&>, 6> const kDirectAccountKeylets{
{{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false},
{.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true},
{.function = &keylet::signerList, .expectedLEName = jss::SignerList, .includeInTests = true},
// It's normally impossible to create an item at nftpage_min, but
// test it anyway, since the invariant checks for it.
{.function = &keylet::nftokenPageMin,
.expectedLEName = jss::NFTokenPage,
.includeInTests = true},
{.function = &keylet::nftokenPageMax,
.expectedLEName = jss::NFTokenPage,
.includeInTests = true},
{.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}};
/**
* Type-specific prefix for calculating ledger indices.
*

View File

@@ -1,10 +1,12 @@
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <functional>
#include <map>
@@ -863,6 +865,101 @@ public:
//--------------------------------------------------------------------------
void
testIssueFromJson()
{
testcase("issueFromJson");
// Valid XRP — no issuer field
{
json::Value jv;
jv[jss::currency] = "XRP";
auto const issue = issueFromJson(jv);
BEAST_EXPECT(isXRP(issue));
}
// Valid IOU — legitimate issuer
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
auto const issue = issueFromJson(jv);
BEAST_EXPECT(!isXRP(issue));
BEAST_EXPECT(issue.account != noAccount());
}
// noAccount() is the MPT sentinel in binary serialization - must be
// rejected
try
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(noAccount());
issueFromJson(jv);
fail("noAccount() accepted as IOU issuer");
}
catch (...)
{
pass();
}
// xrpAccount() is the XRP sentinel (all zeros) - must be rejected
// as IOU issuer
try
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(xrpAccount());
issueFromJson(jv);
fail("xrpAccount() accepted as IOU issuer");
}
catch (...)
{
pass();
}
// Invalid base58 — must be rejected
try
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = "not_a_valid_address";
issueFromJson(jv);
fail("invalid base58 accepted as IOU issuer");
}
catch (...)
{
pass();
}
// Non-XRP currency with no issuer field — must be rejected
try
{
json::Value jv;
jv[jss::currency] = "USD";
issueFromJson(jv);
fail("missing issuer accepted");
}
catch (...)
{
pass();
}
// XRP with an issuer field — must be rejected
try
{
json::Value jv;
jv[jss::currency] = "XRP";
jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
issueFromJson(jv);
fail("XRP with issuer accepted");
}
catch (...)
{
pass();
}
}
void
run() override
{
@@ -897,6 +994,9 @@ public:
// ---
testIssueDomainSets();
testIssueDomainMaps();
// ---
testIssueFromJson();
}
};

View File

@@ -1,15 +1,24 @@
#include <test/jtx/Account.h>
#include <test/jtx/Env.h>
#include <test/jtx/amount.h> // IWYU pragma: keep
#include <test/jtx/envconfig.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STIssue.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <memory>
namespace xrpl::test {
@@ -137,12 +146,143 @@ public:
"000000000000000000000000000000000000000000000002");
}
void
testNoAccountIssuerRpc()
{
testcase("noAccount issuer rejected via RPC sign");
using namespace jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString("[signing_support]\ntrue");
return cfg;
})};
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
json::Value txJson;
txJson[jss::TransactionType] = "AMMDelete";
txJson[jss::Account] = alice.human();
txJson[jss::Asset][jss::currency] = "USD";
txJson[jss::Asset][jss::issuer] = to_string(noAccount());
txJson[jss::Asset2][jss::currency] = "XRP";
json::Value req;
req[jss::tx_json] = txJson;
req[jss::secret] = alice.name();
auto const result = env.rpc("json", "sign", to_string(req))[jss::result];
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Field 'tx_json.Asset' has invalid data.");
}
void
testNoAccountIssuer()
{
testcase("noAccount issuer rejection");
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(noAccount());
try
{
issueFromJson(sfAsset, jv);
fail("issueFromJson accepted noAccount() as IOU issuer");
}
catch (...)
{
pass();
}
}
{
Serializer s;
s.addBitString(toCurrency("USD"));
s.addBitString(noAccount());
SerialIter iter(s.slice());
try
{
STIssue const stissue(iter, sfAsset);
fail(
"STIssue deserialization of [USD][noAccount()] should "
"throw");
}
catch (...)
{
pass();
}
}
}
void
testXrpAccountIssuerRpc()
{
testcase("xrpAccount issuer rejected via RPC sign");
using namespace jtx;
Env env{*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->loadFromString("[signing_support]\ntrue");
return cfg;
})};
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
json::Value txJson;
txJson[jss::TransactionType] = "AMMDelete";
txJson[jss::Account] = alice.human();
txJson[jss::Asset][jss::currency] = "USD";
txJson[jss::Asset][jss::issuer] = to_string(xrpAccount());
txJson[jss::Asset2][jss::currency] = "XRP";
json::Value req;
req[jss::tx_json] = txJson;
req[jss::secret] = alice.name();
auto const result = env.rpc("json", "sign", to_string(req))[jss::result];
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Field 'tx_json.Asset' has invalid data.");
}
void
testXrpAccountIssuer()
{
testcase("xrpAccount issuer rejection");
{
json::Value jv;
jv[jss::currency] = "USD";
jv[jss::issuer] = to_string(xrpAccount());
try
{
issueFromJson(sfAsset, jv);
fail("issueFromJson accepted xrpAccount() as IOU issuer");
}
catch (...)
{
pass();
}
}
}
void
run() override
{
// compliments other unit tests to ensure complete coverage
testConstructor();
testCompare();
testNoAccountIssuerRpc();
testNoAccountIssuer();
testXrpAccountIssuerRpc();
testXrpAccountIssuer();
}
};

View File

@@ -27,6 +27,7 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/nft.h>
@@ -36,6 +37,7 @@
#include <cstdint>
#include <iterator>
#include <optional>
#include <ranges>
#include <vector>
namespace xrpl::test {
@@ -1616,6 +1618,86 @@ public:
}
}
void
testAccountObjectDoesntShowCancelledOffers()
{
testcase("AccountObjectDoesntShowCancelledOffers");
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
auto const eur = bob["EUR"];
env.fund(XRP(10000), alice, bob);
env.close();
auto const rpcAccountObjects = [&](std::optional<uint32_t> limit = std::nullopt) {
json::Value params;
params[jss::account] = alice.human();
if (limit.has_value())
{
params[jss::limit] = *limit;
}
return env.rpc("json", "account_objects", to_string(params));
};
auto const numEntries = 33;
std::vector<uint32_t> seqs;
seqs.reserve(numEntries);
for ([[maybe_unused]] auto _ : std::ranges::iota_view{0, numEntries})
{
json::Value params;
params[jss::secret] = toBase58(generateSeed("alice"));
params[jss::tx_json] = offer(alice, eur(1), XRP(2));
auto const res = env.rpc("json", "submit", to_string(params))[jss::result];
BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS");
seqs.push_back(env.seq(alice));
}
auto res = rpcAccountObjects();
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == numEntries);
BEAST_EXPECT(not res[jss::result].isMember(jss::limit));
BEAST_EXPECT(not res[jss::result].isMember(jss::marker));
for (auto const s : std::views::all(seqs) | std::views::take(numEntries - 1))
{
json::Value params;
params[jss::secret] = toBase58(generateSeed("alice"));
params[jss::tx_json] = offerCancel(alice, s - 1);
auto const res = env.rpc("json", "submit", to_string(params))[jss::result];
BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS");
}
res = rpcAccountObjects();
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1);
BEAST_EXPECT(not res[jss::result].isMember(jss::limit));
BEAST_EXPECT(not res[jss::result].isMember(jss::marker));
{
json::Value params;
params[jss::secret] = toBase58(generateSeed("alice"));
json::Value txJson;
txJson[jss::TransactionType] = jss::NFTokenMint;
txJson[jss::Account] = to_string(alice.id());
txJson["NFTokenTaxon"] = 1;
params[jss::tx_json] = txJson;
auto const res = env.rpc("json", "submit", to_string(params))[jss::result];
BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS");
}
env.close();
res = rpcAccountObjects();
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 2);
BEAST_EXPECT(not res[jss::result].isMember(jss::limit));
BEAST_EXPECT(not res[jss::result].isMember(jss::marker));
res = rpcAccountObjects(1);
BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1);
BEAST_EXPECT(res[jss::result][jss::limit].asUInt() == 1);
BEAST_EXPECT(res[jss::result].isMember(jss::marker));
}
void
run() override
{
@@ -1627,6 +1709,7 @@ public:
testAccountNFTs();
testAccountObjectMarker();
testSponsoredFilter();
testAccountObjectDoesntShowCancelledOffers();
}
};

View File

@@ -5,6 +5,7 @@
#include <test/jtx/amount.h>
#include <test/jtx/balance.h> // IWYU pragma: keep
#include <test/jtx/check.h>
#include <test/jtx/delegate.h>
#include <test/jtx/deposit.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/fee.h>
@@ -28,6 +29,7 @@
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ApiVersion.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
@@ -45,6 +47,7 @@
#include <initializer_list>
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <utility>
@@ -889,6 +892,426 @@ class AccountTx_test : public beast::unit_test::Suite
checkAliceAcctTx(9, jss::Payment);
}
void
testDelegation()
{
testcase("Delegation Filtering");
using namespace test::jtx;
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, bob, carol);
env.close();
// Normal TX: Alice pays Carol (Signed by Alice's Master Key)
env(pay(alice, carol, XRP(10)));
env.close();
// Setup Delegation: Alice allows Bob to sign Payments for her
env(delegate::set(alice, bob, {"Payment"}));
env.close();
// Delegated TX: Alice pays Bob (Signed by Bob using Delegation)
env(pay(alice, bob, XRP(20)), delegate::As(bob));
env.close();
// Normal TX: Bob pays Carol (Signed by Bob for himself)
env(pay(bob, carol, XRP(30)));
env.close();
auto const countTxs = [&](AccountID const& account,
json::Value const& delegateParams,
std::optional<std::uint32_t> const limit = std::nullopt) -> int {
int count = 0;
json::Value marker;
bool haveMarker = false;
int pages = 0;
while (true)
{
json::Value params;
params[jss::account] = toBase58(account);
params[jss::ledger_index_min] = -1;
params[jss::ledger_index_max] = -1;
if (!delegateParams.isNull())
params[jss::delegate] = delegateParams;
if (limit)
params[jss::limit] = *limit;
if (haveMarker)
params[jss::marker] = marker;
auto const res = env.rpc("json", "account_tx", to_string(params));
auto const& result = res[jss::result];
if (result.isMember(jss::transactions))
count += result[jss::transactions].size();
if (!limit || !result.isMember(jss::marker))
break;
marker = result[jss::marker];
haveMarker = true;
++pages;
if (!BEAST_EXPECT(pages < 20))
break;
}
return count;
};
auto const checkError = [&](json::Value const& delegateParams,
std::string const& errToken) {
json::Value params;
params[jss::account] = alice.human();
params[jss::delegate] = delegateParams;
auto res = env.rpc("json", "account_tx", to_string(params));
BEAST_EXPECT(res[jss::result][jss::error] == errToken);
};
// Filter: Delegatee. Expects TX #2 (Signed by Bob)
{
json::Value p;
p[jss::delegate_filter] = "actor";
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
}
// Filter: Delegatee + Counterparty Bob. Expects TX #2.
{
json::Value p;
p[jss::delegate_filter] = "actor";
p[jss::counter_party] = bob.human();
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
}
// Filter: Delegatee + Counterparty Carol. Expects 0.
{
json::Value p;
p[jss::delegate_filter] = "actor";
p[jss::counter_party] = carol.human();
BEAST_EXPECT(countTxs(alice.id(), p) == 0);
}
// Filter: Delegator. Expects TX #2.
// (Bob signed it, but Alice is the owner).
{
json::Value p;
p[jss::delegate_filter] = "authorizer";
BEAST_EXPECT(countTxs(bob.id(), p) == 1);
}
// Filter: Delegator + Counterparty Alice. Expects TX #2.
{
json::Value p;
p[jss::delegate_filter] = "authorizer";
p[jss::counter_party] = alice.human();
BEAST_EXPECT(countTxs(bob.id(), p) == 1);
}
// Filter: Authorizer. Expect: None.
// TX #2 has sfDelegate present, but Alice is the delegator/owner, not
// the delegate signer
{
json::Value p;
p[jss::delegate_filter] = "authorizer";
BEAST_EXPECT(countTxs(alice.id(), p) == 0);
}
// Query Bob (Signer), Filter: Delegator, Counterparty: Carol
// Expect: None (Alice is Owner, not Carol)
{
json::Value p;
p[jss::delegate_filter] = "authorizer";
p[jss::counter_party] = carol.human();
BEAST_EXPECT(countTxs(bob.id(), p) == 0);
}
// Query Bob (Signer), Filter: Delegatee
// Expect: None. Bob did not employ a delegatee for his own TXs (TX C).
{
json::Value p;
p[jss::delegate_filter] = "actor";
BEAST_EXPECT(countTxs(bob.id(), p) == 0);
}
// "delegate" is not an object (e.g., string)
{
json::Value const p = "not_an_object";
checkError(p, "invalidParams");
}
// Missing "delegate_filter" inside object
{
json::Value const p(json::ValueType::Object);
checkError(p, "invalidParams");
}
// "delegate_filter" is not a string (e.g., int)
{
json::Value p;
p[jss::delegate_filter] = 123;
checkError(p, "invalidParams");
}
// "delegate_filter" has invalid value
{
json::Value p;
p[jss::delegate_filter] = "random_string";
checkError(p, "invalidParams");
}
// "counterparty" is not a string
{
json::Value p;
p[jss::delegate_filter] = "actor";
p[jss::counter_party] = 123;
checkError(p, "invalidParams");
}
// "counterparty" is malformed base58
{
json::Value p;
p[jss::delegate_filter] = "actor";
p[jss::counter_party] = "not_an_account";
checkError(p, "actMalformed");
}
// Multi-signed non-delegated TX: Alice pays Carol via multi-sig.
// sfDelegate is absent and sfSigningPubKey is empty — the filter
// must skip it without crashing.
{
Account const daria{"daria"};
Account const edward{"edward"};
env.fund(XRP(1000), daria, edward);
env.close();
env(signers(alice, 2, {{daria, 1}, {edward, 1}}));
env.close();
env(pay(alice, carol, XRP(1)),
Fee(drops(env.current()->fees().increment * 2)),
Msig(daria, edward));
env.close();
// Alice's actor filter should still see only the 1 delegated tx,
// not the multi-signed one.
json::Value p;
p[jss::delegate_filter] = "actor";
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
}
// Regular-key-signed non-delegated TX: Alice pays Bob, signed by Bob
// as Alice's regular key. This must not be treated as delegation.
{
env(regkey(alice, bob));
env.close();
env(pay(alice, bob, XRP(1)));
env.close();
json::Value actorFilter;
actorFilter[jss::delegate_filter] = "actor";
BEAST_EXPECT(countTxs(alice.id(), actorFilter) == 1);
// limit: 1 forces pagination past newer non-delegated rows.
BEAST_EXPECT(countTxs(alice.id(), actorFilter, 1) == 1);
actorFilter[jss::counter_party] = bob.human();
BEAST_EXPECT(countTxs(alice.id(), actorFilter) == 1);
BEAST_EXPECT(countTxs(alice.id(), actorFilter, 1) == 1);
json::Value authorizerFilter;
authorizerFilter[jss::delegate_filter] = "authorizer";
BEAST_EXPECT(countTxs(bob.id(), authorizerFilter) == 1);
BEAST_EXPECT(countTxs(bob.id(), authorizerFilter, 1) == 1);
authorizerFilter[jss::counter_party] = alice.human();
BEAST_EXPECT(countTxs(bob.id(), authorizerFilter) == 1);
BEAST_EXPECT(countTxs(bob.id(), authorizerFilter, 1) == 1);
}
// Pagination marker/delegate-filter consistency. A marker returned by a
// delegate-filtered query carries a `delegate` flag and is only valid
// for a follow-up request that repeats the filter; mixing the two
// marker conventions must be rejected with invalidParams.
{
json::Value actorFilter;
actorFilter[jss::delegate_filter] = "actor";
// Obtain a delegate-filtered marker (limit 1 forces pagination).
json::Value dp;
dp[jss::account] = alice.human();
dp[jss::ledger_index_min] = -1;
dp[jss::ledger_index_max] = -1;
dp[jss::delegate] = actorFilter;
dp[jss::limit] = 1;
auto const dRes = env.rpc("json", "account_tx", to_string(dp));
BEAST_EXPECT(dRes[jss::result].isMember(jss::marker));
json::Value const delegateMarker = dRes[jss::result][jss::marker];
BEAST_EXPECT(
delegateMarker.isMember(jss::delegate) && delegateMarker[jss::delegate].asBool());
// Reusing a delegate marker without the delegate filter is rejected.
{
json::Value p;
p[jss::account] = alice.human();
p[jss::ledger_index_min] = -1;
p[jss::ledger_index_max] = -1;
p[jss::limit] = 1;
p[jss::marker] = delegateMarker;
auto const r = env.rpc("json", "account_tx", to_string(p));
BEAST_EXPECT(r[jss::result][jss::error] == "invalidParams");
}
// Obtain a non-delegate marker; it must not carry the flag.
json::Value np;
np[jss::account] = alice.human();
np[jss::ledger_index_min] = -1;
np[jss::ledger_index_max] = -1;
np[jss::limit] = 1;
auto const nRes = env.rpc("json", "account_tx", to_string(np));
BEAST_EXPECT(nRes[jss::result].isMember(jss::marker));
json::Value const normalMarker = nRes[jss::result][jss::marker];
BEAST_EXPECT(!normalMarker.isMember(jss::delegate));
// Reusing a non-delegate marker with a delegate filter is rejected.
{
json::Value p;
p[jss::account] = alice.human();
p[jss::ledger_index_min] = -1;
p[jss::ledger_index_max] = -1;
p[jss::limit] = 1;
p[jss::delegate] = actorFilter;
p[jss::marker] = normalMarker;
auto const r = env.rpc("json", "account_tx", to_string(p));
BEAST_EXPECT(r[jss::result][jss::error] == "invalidParams");
}
}
}
void
testDelegationMultiSign()
{
testcase("Delegation filter with multi-signed delegatee");
using namespace test::jtx;
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
Account const daria{"daria"};
Account const edward{"edward"};
env.fund(XRP(10000), alice, bob, carol, daria, edward);
env.close();
// Bob's identity is established via multi-sig (daria + edward)
env(signers(bob, 2, {{daria, 1}, {edward, 1}}));
env.close();
env(delegate::set(alice, bob, {"Payment"}));
env.close();
// Delegated tx: Alice pays Carol, Bob signs via multi-sig
env(pay(alice, carol, XRP(10)), Fee(XRP(1)), delegate::As(bob), Msig(daria, edward));
env.close();
auto const countTxs = [&](AccountID const& account,
json::Value const& delegateParams) -> int {
json::Value params;
params[jss::account] = toBase58(account);
params[jss::ledger_index_min] = -1;
params[jss::ledger_index_max] = -1;
params[jss::delegate] = delegateParams;
auto const res = env.rpc("json", "account_tx", to_string(params));
if (res[jss::result].isMember(jss::transactions))
return res[jss::result][jss::transactions].size();
return 0;
};
// Alice (owner) finds the tx with actor filter
{
json::Value p;
p[jss::delegate_filter] = "actor";
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
}
// Alice (owner) + counterparty Bob finds the tx
{
json::Value p;
p[jss::delegate_filter] = "actor";
p[jss::counter_party] = bob.human();
BEAST_EXPECT(countTxs(alice.id(), p) == 1);
}
// Bob (delegatee) finds the tx with authorizer filter
{
json::Value p;
p[jss::delegate_filter] = "authorizer";
BEAST_EXPECT(countTxs(bob.id(), p) == 1);
}
// Bob (delegatee) + counterparty Alice finds the tx
{
json::Value p;
p[jss::delegate_filter] = "authorizer";
p[jss::counter_party] = alice.human();
BEAST_EXPECT(countTxs(bob.id(), p) == 1);
}
}
void
testDelegationMarkerWithinPage()
{
testcase("Delegation filter marker within a single query page");
using namespace test::jtx;
Env env(*this);
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, bob, carol);
env.close();
env(delegate::set(alice, bob, {"Payment"}));
env.close();
auto const startLedger = env.closed()->header().seq + 1;
env(pay(alice, carol, XRP(1)), delegate::As(bob));
env.close();
env(pay(alice, carol, XRP(1)), delegate::As(bob));
env.close();
json::Value p;
p[jss::delegate_filter] = "actor";
json::Value params;
params[jss::account] = alice.human();
params[jss::ledger_index_min] = startLedger;
params[jss::ledger_index_max] = -1;
params[jss::delegate] = p;
params[jss::limit] = 1;
auto const res = env.rpc("json", "account_tx", to_string(params));
auto const& result = res[jss::result];
// The first page emits only the first delegated payment. (as page limit is set to 1)
BEAST_EXPECT(result[jss::transactions].size() == 1);
BEAST_EXPECT(result.isMember(jss::marker));
// Following the marker resumes right after the first payment and
// returns the second one.
json::Value page2 = params;
page2[jss::marker] = result[jss::marker];
auto const res2 = env.rpc("json", "account_tx", to_string(page2));
BEAST_EXPECT(res2[jss::result][jss::transactions].size() == 1);
}
void
testSponsorship()
{
@@ -973,6 +1396,9 @@ public:
testContents();
testAccountDelete();
testMPT();
testDelegation();
testDelegationMultiSign();
testDelegationMarkerWithinPage();
testSponsorship();
}
};

View File

@@ -2,6 +2,7 @@
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/multisign.h>
#include <test/jtx/noop.h>
#include <test/jtx/pay.h>
#include <test/jtx/seq.h>
@@ -880,6 +881,125 @@ class Transaction_test : public beast::unit_test::Suite
}
}
void
testSignForNetworkIDValidation()
{
testcase("SignFor NetworkID validation");
using namespace test::jtx;
Account const owner{"owner"};
Account const signer{"signer"};
auto makeConfig = [](std::uint32_t networkID) {
return envconfig([networkID](std::unique_ptr<Config> cfg) {
cfg->networkId = networkID;
return cfg;
});
};
auto setupEnv = [&](Env& env) {
env.fund(XRP(10'000), owner, signer);
env.close();
env(signers(owner, 1, {{signer, 1}}));
env.close();
};
auto makeTx = [&](Env& env) {
json::Value tx;
tx[jss::TransactionType] = jss::AccountSet;
tx[jss::Account] = owner.human();
tx[jss::Sequence] = env.seq(owner);
tx[jss::Fee] = "100";
tx[jss::SigningPubKey] = "";
return tx;
};
auto signFor = [&](Env& env, json::Value const& tx) {
json::Value signReq;
signReq[jss::tx_json] = tx;
signReq[jss::account] = signer.human();
signReq[jss::secret] = signer.name();
return env.rpc("json", "sign_for", to_string(signReq))[jss::result];
};
// Test case: NetworkID < 1024 - field is not required
{
Env env{*this, makeConfig(500)};
setupEnv(env);
auto tx = makeTx(env);
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::status] == "success");
BEAST_EXPECT(!result[jss::tx_json].isMember(jss::NetworkID));
}
// Test case: NetworkID > 1024 - missing NetworkID field
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Missing field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is not a number
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = "not_a_number";
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is not integral
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = 2040.1;
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is different from
// actual NetworkID
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = 9999;
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::error] == "invalidParams");
BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'.");
}
// Test case: NetworkID > 1024 - NetworkID field is correct
{
Env env{*this, makeConfig(2040)};
setupEnv(env);
auto tx = makeTx(env);
tx[jss::NetworkID] = 2040;
auto result = signFor(env, tx);
BEAST_EXPECT(result[jss::status] == "success");
BEAST_EXPECT(result[jss::tx_json][jss::NetworkID].asUInt() == 2040);
}
}
public:
void
run() override
@@ -889,6 +1009,8 @@ public:
FeatureBitset const all{testableAmendments()};
testWithFeats(all);
testSignForNetworkIDValidation();
}
void

View File

@@ -122,6 +122,73 @@ struct BaseUintTest : public ::testing::Test
}
};
using BaseUintDeathTest = BaseUintTest;
TEST_F(BaseUintDeathTest, fromRaw_size_mismatch)
{
// ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts. Rather than twist
// these tests into knots to make them work, just skip them.
#ifdef ENABLE_VOIDSTAR
GTEST_SKIP() << "ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts.";
#else
auto smallConstruct = [] {
// Container smaller than the base_uint (8 bytes vs 12 bytes for
// test96). Only the first 8 bytes are copied; the remaining 4 bytes
// stay zero.
Blob const tooSmall{1, 2, 3, 4, 5, 6, 7, 8};
BaseUInt96 const result = BaseUInt96::fromRaw(tooSmall);
auto const resultText = to_string(result);
EXPECT_EQ(resultText, "010203040506070800000000") << resultText;
};
EXPECT_DEBUG_DEATH(smallConstruct(), "input size match");
auto largeConstruct = [] {
// Container larger than the base_uint (16 bytes vs 12 bytes for
// test96). Only the first 12 bytes are copied; the extra bytes are
// ignored.
Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
BaseUInt96 const result = BaseUInt96::fromRaw(tooBig);
auto const resultText = to_string(result);
EXPECT_EQ(resultText, "0102030405060708090A0B0C") << resultText;
};
EXPECT_DEBUG_DEATH(largeConstruct(), "input size match");
auto smallCopy = [] {
// Container smaller than the base_uint (8 bytes vs 12 bytes for
// test96). Only the first 8 bytes are copied; the remaining 4 bytes
// stay zero.
Blob const tooSmall{1, 2, 3, 4, 5, 6, 7, 8};
BaseUInt96 result{};
--result;
{
auto const originalText = to_string(result);
EXPECT_EQ(originalText, "FFFFFFFFFFFFFFFFFFFFFFFF") << originalText;
}
result = tooSmall;
auto const resultText = to_string(result);
EXPECT_EQ(resultText, "010203040506070800000000") << resultText;
};
EXPECT_DEBUG_DEATH(smallCopy(), "input size match");
auto const largeCopy = [] {
// Container larger than the base_uint (16 bytes vs 12 bytes for
// test96). Only the first 12 bytes are copied; the extra bytes are
// ignored.
Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
BaseUInt96 result{};
--result;
{
auto const originalText = to_string(result);
EXPECT_EQ(originalText, "FFFFFFFFFFFFFFFFFFFFFFFF") << originalText;
}
result = tooBig;
auto const resultText = to_string(result);
EXPECT_EQ(resultText, "0102030405060708090A0B0C") << resultText;
};
EXPECT_DEBUG_DEATH(largeCopy(), "input size match");
#endif
}
TEST_F(BaseUintTest, base_uint)
{
static_assert(!std::is_constructible_v<BaseUInt96, std::complex<double>>);
@@ -198,6 +265,19 @@ TEST_F(BaseUintTest, base_uint)
EXPECT_EQ(d, 0);
}
{
// There are several ways to create a zero. beast::kZero is tested above. Test some
// others.
BaseUInt96 const z1;
EXPECT_EQ(z1, z) << to_string(z1);
BaseUInt96 const z2{};
EXPECT_EQ(z2, z) << to_string(z2);
BaseUInt96 const z3{0u};
EXPECT_EQ(z3, z) << to_string(z3);
}
BaseUInt96 n{z};
n++;
EXPECT_EQ(n, BaseUInt96(1));

View File

@@ -3880,7 +3880,8 @@ NetworkOPsImp::addAccountHistoryJob(SubAccountHistoryInfoWeak subInfo)
.ledgerRange = {.min = minLedger, .max = maxLedger},
.marker = marker,
.limit = 0,
.bAdmin = true};
.bAdmin = true,
.delegate = std::nullopt};
return db.newestAccountTxPage(options);
};

View File

@@ -11,6 +11,7 @@
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/RangeSet.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/contract.h>
@@ -28,6 +29,7 @@
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/LedgerHeader.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TxMeta.h>
@@ -990,6 +992,57 @@ getNewestAccountTxsB(
return getAccountTxsB(session, app, options, true, j);
}
/**
* @brief Determines whether a transaction should be included in account_tx
* results based on a delegation filter.
* @param rawData Serialized transaction blob.
* @param filter The delegate filter specifying the role of the queried account
* (Actor or Authorizer) and an optional counterparty to match against.
* @param contextAccount The account passed to account_tx (the queried account).
* @return True if the transaction passes the filter and should be included,
* false if it should be skipped.
*/
static bool
passesDelegateFilter(
Blob const& rawData,
DelegateFilter const& filter,
AccountID const& contextAccount)
{
SerialIter sit{makeSlice(rawData)};
STTx const tx{sit};
AccountID const txOwner = tx.getAccountID(sfAccount);
if (!tx.isFieldPresent(sfDelegate))
return false;
AccountID const txSigner = tx.getAccountID(sfDelegate);
switch (filter.type)
{
case DelegateType::Actor: {
// Keep txns where the queried account (A) is the owner but
// another account (C) was the delegatee that signed.
bool const isDelegated = (txOwner == contextAccount) && (txSigner != contextAccount);
if (!isDelegated)
return false;
return !filter.counterparty || (txSigner == *filter.counterparty);
}
case DelegateType::Authorizer: {
// Keep txns where the queried account (C) is the signer acting
// on behalf of another account (A, the delegator/owner).
bool const isActingAsDelegate =
(txSigner == contextAccount) && (txOwner != contextAccount);
if (!isActingAsDelegate)
return false;
return !filter.counterparty || (txOwner == *filter.counterparty);
}
}
return false; // LCOV_EXCL_LINE
}
/**
* @brief accountTxPage Searches for the oldest or newest transactions for the
* account that matches the given criteria starting from the provided
@@ -1020,6 +1073,7 @@ accountTxPage(
{
int total = 0;
bool const hasDelegateFilter = options.delegate.has_value();
bool lookingForMarker = options.marker.has_value();
std::uint32_t numberOfResults = 0;
@@ -1107,6 +1161,11 @@ accountTxPage(
{
Blob rawData;
Blob rawMeta;
// Delegate filtering happens after SQL, so skipped rows need their own
// continuation marker accounting.
std::uint32_t fetchedRows = 0;
std::optional<RelationalDatabase::AccountTxMarker> lastEmitted;
std::optional<RelationalDatabase::AccountTxMarker> lastScanned;
// SOCI requires boost::optional (not std::optional) as parameters.
boost::optional<std::uint64_t> ledgerSeq;
@@ -1128,18 +1187,31 @@ accountTxPage(
while (st.fetch())
{
if (hasDelegateFilter)
{
++fetchedRows;
lastScanned = {
.ledgerSeq = rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or(0)),
.txnSeq = txnSeq.value_or(0)};
}
if (lookingForMarker)
{
if (findLedger == ledgerSeq.value_or(0) && findSeq == txnSeq.value_or(0))
{
lookingForMarker = false;
// Delegate markers are continuation cursors for the last
// scanned row, so resume after the marker row.
if (hasDelegateFilter)
continue;
}
else
{
continue;
}
}
else if (numberOfResults == 0)
if (!hasDelegateFilter && numberOfResults == 0)
{
newmarker = {
.ledgerSeq = rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or(0)),
@@ -1165,6 +1237,23 @@ accountTxPage(
rawMeta.clear();
}
if (hasDelegateFilter)
{
if (rawData.empty() ||
!passesDelegateFilter(rawData, options.delegate.value(), options.account))
{
rawData.clear();
rawMeta.clear();
continue;
}
if (numberOfResults == 0)
{
newmarker = lastEmitted;
break;
}
}
// Work around a bug that could leave the metadata missing
if (rawMeta.empty())
onUnsavedLedger(ledgerSeq.value_or(0));
@@ -1186,7 +1275,18 @@ accountTxPage(
--numberOfResults;
total++;
if (hasDelegateFilter)
{
lastEmitted = {
.ledgerSeq = rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or(0)),
.txnSeq = txnSeq.value_or(0)};
}
}
// If this filtered page did not fill the requested number of results,
// still return a marker so the caller can continue scanning later rows.
if (hasDelegateFilter && !newmarker && !lookingForMarker && fetchedRows == queryLimit)
newmarker = lastScanned;
}
return {newmarker, total};

View File

@@ -30,6 +30,7 @@
#include <xrpl/resource/Fees.h>
#include <cstdint>
#include <expected>
#include <memory>
#include <optional>
#include <type_traits>
@@ -38,6 +39,48 @@
namespace xrpl {
static std::expected<DelegateFilter, json::Value>
parseDelegateFilter(json::Value const& delegateNode)
{
if (!delegateNode.isObject())
return std::unexpected(RPC::invalidFieldError(jss::delegate));
if (!delegateNode.isMember(jss::delegate_filter) ||
!delegateNode[jss::delegate_filter].isString())
return std::unexpected(RPC::invalidFieldError(jss::delegate_filter));
auto const& delegateFilterStr = delegateNode[jss::delegate_filter].asString();
auto typeResult = [&] -> std::expected<DelegateType, json::Value> {
if (delegateFilterStr == "actor")
return DelegateType::Actor;
if (delegateFilterStr == "authorizer")
return DelegateType::Authorizer;
return std::unexpected(RPC::invalidFieldError(jss::delegate_filter));
}();
if (!typeResult)
return std::unexpected(typeResult.error());
DelegateType const type = *typeResult;
std::optional<AccountID> counterparty;
if (delegateNode.isMember(jss::counter_party))
{
if (!delegateNode[jss::counter_party].isString())
return std::unexpected(RPC::invalidFieldError(jss::counter_party));
counterparty = parseBase58<AccountID>(delegateNode[jss::counter_party].asString());
if (!counterparty)
return std::unexpected(rpcError(RpcActMalformed));
}
return DelegateFilter{.type = type, .counterparty = counterparty};
}
using TxnsData = RelationalDatabase::AccountTxs;
using TxnsDataBinary = RelationalDatabase::MetaTxsList;
using TxnDataBinary = RelationalDatabase::txnMetaLedgerType;
@@ -231,7 +274,8 @@ doAccountTxHelp(RPC::Context& context, AccountTxArgs const& args)
.ledgerRange = result.ledgerRange,
.marker = result.marker,
.limit = args.limit,
.bAdmin = isUnlimited(context.role)};
.bAdmin = isUnlimited(context.role),
.delegate = args.delegate};
auto& db = context.app.getRelationalDatabase();
@@ -370,6 +414,9 @@ populateJsonResponse(
response[jss::marker] = json::ValueType::Object;
response[jss::marker][jss::ledger] = result.marker->ledgerSeq;
response[jss::marker][jss::seq] = result.marker->txnSeq;
if (args.delegate)
response[jss::marker][jss::delegate] = true;
}
}
@@ -386,7 +433,17 @@ populateJsonResponse(
// limit: integer, // optional
// marker: object {ledger: ledger_index, seq: txn_sequence} // optional,
// resume previous query
// delegate: object { // optional
// delegate_filter: string, // required; "actor" or "authorizer"
// counter_party: account // optional
// }
// }
//
// Pagination note for delegate-filtered queries: the `delegate` object (both
// `delegate_filter` and `counter_party`) must be supplied unchanged on every
// paginated request until the query completes. A marker returned by a
// delegate-filtered query is only valid for a follow-up request that repeats
// the same `delegate` object
json::Value
doAccountTx(RPC::JsonContext& context)
{
@@ -454,6 +511,38 @@ doAccountTx(RPC::JsonContext& context)
.ledgerSeq = token[jss::ledger].asUInt(), .txnSeq = token[jss::seq].asUInt()};
}
if (params.isMember(jss::delegate))
{
if (auto const filter = parseDelegateFilter(params[jss::delegate]); filter.has_value())
{
args.delegate = *filter;
}
else
{
return filter.error();
}
}
// A marker produced by a delegate-filtered query uses a different
// pagination cursor than a normal query, so it is only valid when the same
// `delegate` object is supplied again. Reject any mismatch so pagination
// cannot silently skip or duplicate results.
if (args.marker)
{
bool const markerFromDelegate = params[jss::marker].isMember(jss::delegate) &&
params[jss::marker][jss::delegate].isBool() &&
params[jss::marker][jss::delegate].asBool();
if (markerFromDelegate != args.delegate.has_value())
{
RPC::Status const status{
RpcInvalidParams,
"Do not mix delegate and non-delegate pagination markers in account_tx; "
"repeat the same `delegate` object when using a delegate marker."};
status.inject(response);
return response;
}
}
auto res = doAccountTxHelp(context, args);
JLOG(context.j.debug()) << __func__ << " populating response";
return populateJsonResponse(res, args, context);