Compare commits

...

189 Commits

Author SHA1 Message Date
Ed Hennis
a5b7471af6 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-20 17:49:20 -04:00
yinyiqian1
4b198cd5bb fix: Disallow MPTClearRequireAuth if is set (#6712)
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
2026-04-20 21:25:52 +00:00
Ed Hennis
e0734986dd Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-20 15:44:37 -04:00
Alex Kremer
726f20c8f6 feat: Add GRPC TLS support (#6374)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 17:12:14 +00:00
Zhiyuan Wang
96643bb0fa fix: Check for empty sfAdditionalBooks array in hybrid offer invariant (#6716) 2026-04-20 17:10:28 +00:00
chuanshanjida
e83818241a chore: Remove repetitive word in multiple files (#6978)
Signed-off-by: chuanshanjida <chuanshanjida@outlook.com>
2026-04-20 16:56:03 +00:00
Ed Hennis
8440f479e5 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-20 11:37:54 -04:00
Ed Hennis
ac390622e0 Fix clang-tidy issues 2026-04-17 17:54:37 -04:00
Ed Hennis
5d807f0d6d Merge remote-tracking branch 'XRPLF/develop' into ximinez/online-delete-gaps
* XRPLF/develop:
  chore: Enable clang-tidy include cleaner (6947)
  fix: Change AMMClawback return code to tecNO_PERMISSION (6946)
  ci: [DEPENDABOT] bump actions/upload-pages-artifact from 4.0.0 to 5.0.0 (6927)
  ci: [DEPENDABOT] bump actions/upload-artifact from 7.0.0 to 7.0.1 (6928)
  chore: Enable clang-tidy readability checks (6930)
2026-04-17 15:23:33 -04:00
Ed Hennis
b93294f26b Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-16 13:44:23 -04:00
Ed Hennis
ef95ace0f9 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-15 19:06:15 -04:00
Ed Hennis
fc58bf6edf Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-15 14:28:41 -04:00
Ed Hennis
348555d5ba Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-13 20:28:29 -04:00
Ed Hennis
302802e42d Fix clang-tidy issues 2026-04-13 20:23:23 -04:00
Ed Hennis
5d881f87a3 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-10 12:12:27 -04:00
Ed Hennis
a8a8035b32 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-10 09:10:39 -04:00
Ed Hennis
45af14231f Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-09 11:39:55 -04:00
Ed Hennis
380bf274d0 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-08 16:56:56 -04:00
Ed Hennis
460ec5eeea Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-08 15:12:46 -04:00
Ed Hennis
14be8ca4ea Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-07 16:47:32 -04:00
Ed Hennis
948264d44c Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-06 19:06:51 -04:00
Ed Hennis
9c816b2043 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-06 18:33:20 -04:00
Ed Hennis
f68402acd1 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-06 16:52:50 -04:00
Ed Hennis
5358e25eaa Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-06 13:24:22 -04:00
Ed Hennis
a34d1e5537 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-01 17:26:14 -04:00
Ed Hennis
78a122943d Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-01 13:39:05 -04:00
Ed Hennis
06135e7203 Merge branch 'develop' into ximinez/online-delete-gaps 2026-04-01 11:45:32 -04:00
Ed Hennis
f20425fa4f Merge branch 'develop' into ximinez/online-delete-gaps 2026-03-30 21:29:37 -04:00
Ed Hennis
3e94546acc Merge branch 'develop' into ximinez/online-delete-gaps 2026-03-12 15:05:04 -04:00
Ed Hennis
3b088ed0dc Merge branch 'develop' into ximinez/online-delete-gaps 2026-03-10 12:46:18 -04:00
Ed Hennis
26182ed52e Merge branch 'develop' into ximinez/online-delete-gaps 2026-03-06 13:04:00 -04:00
Ed Hennis
de2a3e10f5 Merge branch 'develop' into ximinez/online-delete-gaps 2026-03-05 21:34:29 -04:00
Ed Hennis
e17f8554fc Merge branch 'develop' into ximinez/online-delete-gaps 2026-03-04 17:11:27 -04:00
Ed Hennis
386a7192ba Merge branch 'develop' into ximinez/online-delete-gaps 2026-03-03 20:46:41 -04:00
Ed Hennis
12cc6e424d Merge branch 'develop' into ximinez/online-delete-gaps 2026-03-03 15:54:32 -04:00
Ed Hennis
c9deecf1b7 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-24 17:34:32 -04:00
Ed Hennis
ddd1b49f38 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-24 16:45:55 -04:00
Ed Hennis
c1b2a24005 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-20 18:49:39 -04:00
Ed Hennis
86d88eca31 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-20 18:25:58 -04:00
Ed Hennis
ef09eaea00 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-20 17:31:41 -04:00
Ed Hennis
c504cfb291 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-20 17:21:03 -04:00
Ed Hennis
0c217dfa2b Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-20 15:14:23 -04:00
Ed Hennis
b0198d2566 Merge remote-tracking branch 'upstream/develop' into ximinez/online-delete-gaps
* upstream/develop:
  ci: Add dependabot config (6379)
  Fix tautological assertion (6393)
  chore: Apply clang-format width 100 (6387)
2026-02-20 12:20:07 -05:00
Ed Hennis
7eee8ca802 Update formatting 2026-02-20 12:15:30 -05:00
Ed Hennis
2a079a0154 Merge commit '25cca465538a56cce501477f9e5e2c1c7ea2d84c' into ximinez/online-delete-gaps
* commit '25cca465538a56cce501477f9e5e2c1c7ea2d84c':
  chore: Set clang-format width to 100 in config file (6387)
2026-02-20 12:12:56 -05:00
Ed Hennis
40989c1178 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-19 16:21:11 -05:00
Ed Hennis
addc831eb3 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-18 18:06:20 -04:00
Ed Hennis
b4efc6d116 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-04 16:29:49 -04:00
Ed Hennis
125d075d6e Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-04 14:16:24 -04:00
Ed Hennis
370a775479 Merge branch 'develop' into ximinez/online-delete-gaps 2026-02-03 16:07:47 -04:00
Ed Hennis
1a2ee706eb Fix formatting 2026-01-28 19:43:23 -05:00
Ed Hennis
2a981357ba Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-28 18:40:24 -04:00
Ed Hennis
1ae475e724 Merge commit '5f638f55536def0d88b970d1018a465a238e55f4' into ximinez/online-delete-gaps
* commit '5f638f55536def0d88b970d1018a465a238e55f4':
  chore: Set ColumnLimit to 120 in clang-format (6288)
2026-01-28 17:38:29 -05:00
Ed Hennis
a3e9401fbc Merge commit '92046785d1fea5f9efe5a770d636792ea6cab78b' into ximinez/online-delete-gaps
* commit '92046785d1fea5f9efe5a770d636792ea6cab78b':
  test: Fix the `xrpl.net` unit test using async read (6241)
  ci: Upload Conan recipes for develop, release candidates, and releases (6286)
  fix: Stop embedded tests from hanging on ARM by using `atomic_flag` (6248)
  fix:  Remove DEFAULT fields that change to the default in associateAsset (6259) (6273)
  refactor: Update Boost to 1.90 (6280)
  refactor: clean up uses of `std::source_location` (6272)
  ci: Pass missing sanitizers input to actions (6266)
  ci: Properly propagate Conan credentials (6265)
  ci: Explicitly set version when exporting the Conan recipe (6264)
  ci: Use plus instead of hyphen for Conan recipe version suffix (6261)
  chore: Detect uninitialized variables in CMake files (6247)
  ci: Run on-trigger and on-pr when generate-version is modified (6257)
  refactor: Enforce 15-char limit and simplify labels for thread naming (6212)
  docs: Update Ripple Bug Bounty public key (6258)
  ci: Add missing commit hash to Conan recipe version (6256)
  fix: Include `<functional>` header in `Number.h` (6254)
  ci: Upload Conan recipe for merges into develop and commits to release (6235)
  Limit reply size on `TMGetObjectByHash` queries (6110)
  ci: remove 'master' branch as a trigger (6234)
  Improve ledger_entry lookups for fee, amendments, NUNL, and hashes (5644)
2026-01-28 17:38:13 -05:00
Ed Hennis
9091469f9e Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-15 13:03:03 -04:00
Ed Hennis
17fa54f1f9 Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-15 12:05:41 -04:00
Ed Hennis
8fb5347c2d Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-14 19:20:11 -04:00
Ed Hennis
6739bf998f Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-13 18:04:18 -04:00
Ed Hennis
6eea38ba67 Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-13 16:15:44 -04:00
Ed Hennis
e9cf88b359 Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-13 15:01:56 -04:00
Ed Hennis
645b203476 Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-12 21:07:23 -04:00
Ed Hennis
be2aff1f4c Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-12 14:51:34 -04:00
Ed Hennis
56ed237e82 Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-11 00:48:27 -04:00
Ed Hennis
fd7b0fd135 Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-08 17:05:28 -04:00
Ed Hennis
e700994891 Merge branch 'develop' into ximinez/online-delete-gaps 2026-01-08 13:03:41 -04:00
Ed Hennis
c76f7029ac Merge remote-tracking branch 'XRPLF/develop' into ximinez/online-delete-gaps
* XRPLF/develop:
  test: add more tests for `ledger_entry` RPC (5858)
  refactor: Rename `rippled.cfg` to `xrpld.cfg` (6098)
  Revert "chore: Pin ruamel.yaml<0.19 in pre-commit-hooks (6166)" (6167)
  chore: Pin ruamel.yaml<0.19 in pre-commit-hooks (6166)
  fix: Remove cryptographic libs from libxrpl Conan package (6163)
2026-01-06 11:27:23 -05:00
Ed Hennis
d535c5fb2a Merge branch 'develop' into ximinez/online-delete-gaps 2025-12-22 17:39:25 -05:00
Ed Hennis
54f860463e Merge branch 'develop' into ximinez/online-delete-gaps 2025-12-18 18:15:15 -05:00
Ed Hennis
950434b8ff Merge branch 'develop' into ximinez/online-delete-gaps 2025-12-17 12:12:39 -05:00
Ed Hennis
ee365e876d Merge branch 'develop' into ximinez/online-delete-gaps 2025-12-12 20:34:29 -05:00
Ed Hennis
c6c59834b9 Update View info() to header() 2025-12-12 15:35:07 -05:00
Ed Hennis
63b47914b8 Merge remote-tracking branch 'XRPLF/develop' into ximinez/online-delete-gaps
* XRPLF/develop:
  refactor: Rename `ripple` namespace to `xrpl` (5982)
  refactor: Move JobQueue and related classes into xrpl.core module (6121)
  refactor: Rename `rippled` binary to `xrpld` (5983)
2025-12-11 15:25:43 -05:00
Ed Hennis
9e02e5be2e Merge branch 'develop' into ximinez/online-delete-gaps 2025-12-10 18:55:14 -05:00
Ed Hennis
093cd70fa1 Merge branch 'develop' into ximinez/online-delete-gaps 2025-12-05 21:12:43 -05:00
Ed Hennis
376d65a483 Merge branch 'develop' into ximinez/online-delete-gaps 2025-12-02 17:21:39 -05:00
Ed Hennis
a0d9a2458e Merge branch 'develop' into ximinez/online-delete-gaps 2025-12-01 14:40:15 -05:00
Ed Hennis
456f639cf7 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-28 15:46:19 -05:00
Ed Hennis
2c559ec2f3 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-27 01:48:33 -05:00
Ed Hennis
619c81f463 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-26 00:24:50 -05:00
Ed Hennis
f1490df960 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-25 14:54:38 -05:00
Ed Hennis
7bdf74de98 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-24 21:48:42 -05:00
Ed Hennis
1743d6fb98 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-24 21:29:52 -05:00
Ed Hennis
ca7a5bb926 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-21 12:47:28 -05:00
Ed Hennis
ce8b1a3f1e Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-18 22:39:06 -05:00
Ed Hennis
486fa75a10 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-15 03:08:18 -05:00
Ed Hennis
f8d68cd3d3 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-13 12:18:08 -05:00
Ed Hennis
ef7a3f5606 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-12 13:59:45 -05:00
Ed Hennis
4f84ed7490 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-10 19:52:42 -05:00
Ed Hennis
d534103131 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-10 15:34:55 -05:00
Ed Hennis
82dff3c2ce Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-08 22:49:26 -05:00
Ed Hennis
30d73eb5ba Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-06 23:50:23 -05:00
Ed Hennis
1b2754bac2 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-05 22:23:07 -05:00
Ed Hennis
cf80cafc75 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-04 18:02:30 -05:00
Ed Hennis
b8897d51de Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-03 18:03:56 -05:00
Ed Hennis
3ff25eeb65 Merge branch 'develop' into ximinez/online-delete-gaps 2025-11-03 12:41:48 -05:00
Ed Hennis
2bbfc4e786 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-31 13:51:15 -04:00
Ed Hennis
2b1eb052e6 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-31 12:51:24 -04:00
Ed Hennis
360e214e54 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-29 14:23:10 -04:00
Ed Hennis
2618afed94 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-29 13:42:21 -04:00
Ed Hennis
698ba2c788 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-29 12:54:16 -04:00
Ed Hennis
b614e99588 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-28 17:38:21 -04:00
Ed Hennis
fe8e4af2fa Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-26 19:12:33 -04:00
Ed Hennis
0a897f1528 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-23 13:24:25 -04:00
Ed Hennis
cf8a3f5779 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-22 11:38:49 -04:00
Ed Hennis
db39a39868 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-21 22:20:04 -04:00
Ed Hennis
37a03d28c2 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-17 18:21:36 -04:00
Ed Hennis
19d275425a Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-16 13:12:08 -04:00
Ed Hennis
88e9045602 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-16 10:48:43 -04:00
Ed Hennis
5adbc536b6 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-10 13:01:32 -04:00
Ed Hennis
e27af94ba9 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-09 15:14:47 -04:00
Ed Hennis
43fe1e7e9c Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-08 14:21:34 -04:00
Ed Hennis
f456a858c8 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-02 11:03:07 -04:00
Ed Hennis
084c3aa88e Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-01 18:10:57 -04:00
Ed Hennis
34f9b63921 Merge branch 'develop' into ximinez/online-delete-gaps 2025-10-01 13:14:22 -04:00
Ed Hennis
bd3de79817 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-30 22:28:53 -04:00
Ed Hennis
304eee2259 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-29 18:34:43 -04:00
Ed Hennis
9e729b7f59 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-29 17:37:16 -04:00
Ed Hennis
dd141468c4 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-29 13:32:03 -04:00
Ed Hennis
933147c21f Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-26 19:25:58 -04:00
Ed Hennis
9201a4f591 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-26 13:41:27 -04:00
Ed Hennis
5adb1e9b8b Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-26 12:09:09 -04:00
Ed Hennis
4df84d7988 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-25 13:27:09 -04:00
Bart
cd87c0968b Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-24 09:35:19 +02:00
Ed Hennis
8a8e7c90bf Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-20 15:44:32 -04:00
Ed Hennis
e806069065 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-18 14:08:23 -04:00
Ed Hennis
ce948cbec0 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-18 12:26:34 -04:00
Ed Hennis
6ed34b3294 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-18 11:54:30 -04:00
Ed Hennis
7161a235ca Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-17 10:49:03 -04:00
Ed Hennis
71463810de Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-16 10:46:37 -04:00
Ed Hennis
e997219a85 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-15 11:13:28 -04:00
Ed Hennis
895cc13fa6 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-11 10:33:14 -04:00
Ed Hennis
8d3c3ca29a Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-10 18:53:24 -04:00
Ed Hennis
9829553807 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-09 17:14:20 -04:00
Ed Hennis
e551f9731a Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-08 11:41:46 -04:00
Ed Hennis
fd827bf58b Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-05 17:44:06 -04:00
Ed Hennis
5a3baba34d Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-04 20:24:55 -04:00
Ed Hennis
c78f5b160f Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-04 16:43:48 -04:00
Ed Hennis
485f78761a Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-04 12:26:55 -04:00
Ed Hennis
23cd2f7b21 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-04 10:14:10 -04:00
Ed Hennis
5753266c43 Merge branch 'develop' into ximinez/online-delete-gaps 2025-09-03 14:04:01 -04:00
Ed Hennis
4722d2607d Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-29 15:52:53 -04:00
Ed Hennis
85b5b4f855 Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-29 10:42:50 -04:00
Ed Hennis
a16f492f0f Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-28 18:17:26 -04:00
Ed Hennis
3633dc632c Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-27 11:14:59 -04:00
Ed Hennis
b3b30c3a86 Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-25 14:10:52 -04:00
Ed Hennis
c78a7684f4 Remove trailing space 2025-08-25 14:09:57 -04:00
Ed Hennis
cf83d92630 Merge remote-tracking branch 'upstream/develop' into ximinez/online-delete-gaps
* upstream/develop:
  chore: Remove codecov token check to support tokenless uploads on forks (5722)
  Set version to 2.6.0-rc3
  Revert "perf: Move mutex to the partition level (5486)"
  chore: Update clang-format and prettier with pre-commit (5709)
  fix(test): handle null metadata for unvalidated tx in Env::meta (5715)
  chore: Workaround for CI build errors on arm64 (5717)
  chore: Fix file formatting (5718)
  fix: Skip notify-clio when running in a fork, reorder config fields (5712)
  chore: Reverts formatting changes to external files, adds formatting changes to proto files (5711)
2025-08-25 14:05:49 -04:00
Ed Hennis
a56b1274d8 Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-21 11:38:54 -04:00
Ed Hennis
ae4bdd0492 Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-19 16:05:17 -04:00
Ed Hennis
e90102dd3b Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-18 12:15:38 -04:00
Ed Hennis
71f0e8db3d Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-08 18:23:24 -04:00
Ed Hennis
638929373a Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-08 11:10:20 -04:00
Ed Hennis
8440654377 Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-06 21:03:00 -04:00
Ed Hennis
9fa66c4741 Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-05 21:16:44 -04:00
Ed Hennis
38a9235145 Merge branch 'develop' into ximinez/online-delete-gaps 2025-08-04 13:04:52 -04:00
Ed Hennis
c7a3cc9108 Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-29 20:33:29 -04:00
Ed Hennis
248337908d Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-29 11:54:16 -04:00
Ed Hennis
3d003619fd Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-28 20:57:13 -04:00
Ed Hennis
f163dca12c Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-24 15:50:09 -04:00
Ed Hennis
6e0ce458e5 Revert "TEMP: Change some logging to fatal to diagnose CI failures"
This reverts commit 69cf18158b.
2025-07-22 19:41:02 -04:00
Ed Hennis
5fae8480f1 Revert "TEMP: Add logging to SHAMapStore test"
This reverts commit fe7d0798a7.
2025-07-22 19:40:58 -04:00
Ed Hennis
e6587d374a fixup! Tweak SHAMapStore test timing more 2025-07-22 19:39:58 -04:00
Ed Hennis
376cc404e0 Tweak SHAMapStore test timing more 2025-07-22 18:44:07 -04:00
Ed Hennis
9898ca638f Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-22 14:09:33 -04:00
Ed Hennis
34b46d8f7c Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-21 18:20:41 -04:00
Ed Hennis
fe7d0798a7 TEMP: Add logging to SHAMapStore test 2025-07-21 18:19:53 -04:00
Ed Hennis
0cecc09d71 Tweak timing of SHAMapStore test 2025-07-21 18:19:28 -04:00
Ed Hennis
e091d55561 Try to fix timing of LedgerMaster test 2025-07-21 15:14:42 -04:00
Ed Hennis
69cf18158b TEMP: Change some logging to fatal to diagnose CI failures 2025-07-21 14:32:08 -04:00
Ed Hennis
6513c53817 Improve logging
- There's still a race condition in there
2025-07-21 14:30:49 -04:00
Ed Hennis
e13baa58a5 Fix build errors 2025-07-21 13:31:09 -04:00
Ed Hennis
951056fe9b Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-18 18:33:10 -04:00
Ed Hennis
67700ea6bd Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-16 12:54:23 -04:00
Ed Hennis
e5442cf3f1 Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-15 19:36:25 -04:00
Ed Hennis
da68076f04 Change default recovery wait time to 1s
See https://github.com/XRPLF/rippled/pull/5531#issuecomment-3058218837
2025-07-14 14:13:32 -04:00
Ed Hennis
b24116a118 Improve locking, logging, and test output
- Add more info to the error message on some failed tests.
- Add logging details to SHAMapStoreImp
2025-07-14 14:13:27 -04:00
Ed Hennis
f67398c6bf Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-11 19:27:31 -04:00
Ed Hennis
43d3eb1a24 Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-10 21:25:22 -04:00
Ed Hennis
0993315ed5 Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-10 12:30:18 -04:00
Ed Hennis
0bc383ada9 Fix build errors 2025-07-08 20:15:05 -04:00
Ed Hennis
1841ceca43 Add more logging to SHAMapStore rotation 2025-07-08 16:05:03 -04:00
Ed Hennis
2714cebabd Revert "TEMP: Change logging to show progress during unit test"
This reverts commit e184db4ce2.
2025-07-08 16:02:45 -04:00
Ed Hennis
e184db4ce2 TEMP: Change logging to show progress during unit test 2025-07-08 16:02:19 -04:00
Ed Hennis
ac6dc6943c Tweak when the starting range of ledger gap detection is set
- Add a test to exercise online delete ledger gap detection
2025-07-08 16:01:23 -04:00
Ed Hennis
ddd53806df Add a test to exercise LedgerMaster::missingFromCompleteLedgerRange 2025-07-08 13:22:47 -04:00
Ed Hennis
e629a1f70e Merge branch 'develop' into ximinez/online-delete-gaps 2025-07-03 15:51:23 -04:00
Ed Hennis
68076d969c fixup! fixup! Pause online delete if there any any gaps in recent ledger history 2025-07-02 19:05:06 -04:00
Ed Hennis
d3009d3e1c fixup! Pause online delete if there any any gaps in recent ledger history 2025-07-02 18:51:42 -04:00
Ed Hennis
54f7f3c894 Pause online delete if there any any gaps in recent ledger history 2025-07-02 18:45:48 -04:00
23 changed files with 1772 additions and 71 deletions

View File

@@ -1033,10 +1033,11 @@
# The online delete process checks periodically
# that xrpld is still in sync with the network,
# and that the validated ledger is less than
# 'age_threshold_seconds' old. If not, then continue
# 'age_threshold_seconds' old, and that all
# recent ledgers are available. If not, then continue
# sleeping for this number of seconds and
# checking until healthy.
# Default is 5.
# Default is 1.
#
# Notes:
# The 'node_db' entry configures the primary, persistent storage.
@@ -1416,6 +1417,12 @@
# in this section to a comma-separated list of the addresses
# of your Clio servers, in order to bypass xrpld's rate limiting.
#
# TLS/SSL can be enabled for gRPC by specifying ssl_cert and ssl_key.
# Both parameters must be provided together. The ssl_cert_chain parameter
# is optional and provides intermediate CA certificates for the certificate
# chain. The ssl_client_ca parameter is optional and enables mutual TLS
# (client certificate verification).
#
# This port is commented out but can be enabled by removing
# the '#' from each corresponding line including the entry under [server]
#
@@ -1465,11 +1472,74 @@ admin = 127.0.0.1
protocol = ws
send_queue_limit = 500
# gRPC TLS/SSL Configuration
#
# The gRPC port supports optional TLS/SSL encryption. When TLS is not
# configured, the gRPC server will accept unencrypted connections.
#
# ssl_cert = <filename>
# ssl_key = <filename>
#
# To enable TLS for gRPC, both ssl_cert and ssl_key must be specified.
# If only one is provided, xrpld will fail to start.
#
# ssl_cert: Path to the server's SSL certificate file in PEM format.
# ssl_key: Path to the server's SSL private key file in PEM format.
#
# When configured, the gRPC server will only accept TLS-encrypted
# connections. Clients must use TLS (secure) channel credentials rather
# than plaintext / insecure connections.
#
# ssl_cert_chain = <filename>
#
# Optional. Path to intermediate CA certificate(s) in PEM format that
# complete the server's certificate chain.
#
# This file should contain the intermediate CA certificate(s) needed
# to build a trust chain from the server certificate (ssl_cert) to a
# root CA that clients trust. Multiple certificates should be
# concatenated in PEM format.
#
# This is needed when your server certificate was signed by an
# intermediate CA rather than directly by a root CA. Without this,
# clients may fail to verify your server certificate.
#
# If not specified, only the server certificate from ssl_cert will be
# presented to clients.
#
# ssl_client_ca = <filename>
#
# Optional. Path to a CA certificate file in PEM format for verifying
# client certificates (mutual TLS / mTLS).
#
# When specified, the gRPC server will verify client certificates
# against this CA. This enables mutual authentication where both the
# server and client verify each other's identity.
#
# This is typically NOT needed for public-facing gRPC servers. Only
# use this if you want to restrict access to clients with valid
# certificates signed by the specified CA.
#
# If not specified, the server will use one-way TLS (server
# authentication only) and will accept connections from any client.
#
[port_grpc]
port = 50051
ip = 127.0.0.1
secure_gateway = 127.0.0.1
# Optional TLS/SSL configuration for gRPC
# To enable TLS, uncomment and configure both ssl_cert and ssl_key:
#ssl_cert = /etc/ssl/certs/grpc-server.crt
#ssl_key = /etc/ssl/private/grpc-server.key
# Optional: Include intermediate CA certificates for complete certificate chain
#ssl_cert_chain = /etc/ssl/certs/grpc-intermediate-ca.crt
# Optional: Enable mutual TLS (client certificate verification)
# Uncomment to require and verify client certificates:
#ssl_client_ca = /etc/ssl/certs/grpc-client-ca.crt
#[port_ws_public]
#port = 6005
#ip = 127.0.0.1

View File

@@ -11,7 +11,8 @@ namespace xrpl {
class ValidPermissionedDEX
{
bool regularOffers_ = false;
bool badHybrids_ = false;
bool badHybridsOld_ = false; // pre-fixSecurity3_1_3: missing field/domain or size > 1
bool badHybrids_ = false; // post-fixSecurity3_1_3: also catches size == 0 (size != 1)
hash_set<uint256> domains_;
public:

View File

@@ -6,6 +6,7 @@
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
@@ -61,10 +62,26 @@ offerInDomain(
if (sleOffer->getFieldH256(sfDomainID) != domainID)
return false; // LCOV_EXCL_LINE
if (sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks))
if (view.rules().enabled(fixSecurity3_1_3))
{
JLOG(j.error()) << "Hybrid offer " << offerID << " missing AdditionalBooks field";
return false; // LCOV_EXCL_LINE
// post-fixSecurity3_1_3: also catches empty sfAdditionalBooks (size == 0)
if (sleOffer->isFlag(lsfHybrid) &&
(!sleOffer->isFieldPresent(sfAdditionalBooks) ||
sleOffer->getFieldArray(sfAdditionalBooks).size() != 1))
{
JLOG(j.error()) << "Hybrid offer " << offerID
<< " missing or malformed AdditionalBooks field";
return false; // LCOV_EXCL_LINE
}
}
else
{
// pre-fixSecurity3_1_3: only check for missing sfAdditionalBooks
if (sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks))
{
JLOG(j.error()) << "Hybrid offer " << offerID << " missing AdditionalBooks field";
return false; // LCOV_EXCL_LINE
}
}
return accountInDomain(view, sleOffer->getAccountID(sfAccount), domainID);

View File

@@ -3,6 +3,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
@@ -40,11 +41,17 @@ ValidPermissionedDEX::visitEntry(
regularOffers_ = true;
}
// if a hybrid offer is missing domain or additional book, there's
// something wrong
// pre-fixSecurity3_1_3: hybrid offer missing domain, missing
// sfAdditionalBooks, or sfAdditionalBooks has more than one entry
if (after->isFlag(lsfHybrid) &&
(!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
after->getFieldArray(sfAdditionalBooks).size() > 1))
badHybridsOld_ = true;
// post-fixSecurity3_1_3: same as above but also catches size == 0
if (after->isFlag(lsfHybrid) &&
(!after->isFieldPresent(sfDomainID) || !after->isFieldPresent(sfAdditionalBooks) ||
after->getFieldArray(sfAdditionalBooks).size() != 1))
badHybrids_ = true;
}
}
@@ -63,7 +70,8 @@ ValidPermissionedDEX::finalize(
// For each offercreate transaction, check if
// permissioned offers are valid
if (txType == ttOFFER_CREATE && badHybrids_)
bool const isMalformed = view.rules().enabled(fixSecurity3_1_3) ? badHybrids_ : badHybridsOld_;
if (txType == ttOFFER_CREATE && isMalformed)
{
JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed";
return false;

View File

@@ -594,7 +594,7 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
auto const cancelSequence = ctx_.tx[~sfOfferSequence];
// Note that we we use the value from the sequence or ticket as the
// Note that we use the value from the sequence or ticket as the
// offer sequence. For more explanation see comments in SeqProxy.h.
auto const offerSequence = ctx_.tx.getSeqValue();

View File

@@ -439,7 +439,7 @@ EscrowCreate::doApply()
return tecDST_TAG_NEEDED;
}
// Create escrow in ledger. Note that we we use the value from the
// Create escrow in ledger. Note that we use the value from the
// sequence or ticket. For more explanation see comments in SeqProxy.h.
Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(escrowKeylet);

View File

@@ -134,7 +134,7 @@ PaymentChannelCreate::doApply()
// Create PayChan in ledger.
//
// Note that we we use the value from the sequence or ticket as the
// Note that we use the value from the sequence or ticket as the
// payChan sequence. For more explanation see comments in SeqProxy.h.
Keylet const payChanKeylet = keylet::payChan(account, dst, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(payChanKeylet);

View File

@@ -231,6 +231,12 @@ MPTokenIssuanceSet::preclaim(PreclaimContext const& ctx)
((*mutableFlags & (f.setFlag | f.clearFlag)));
}))
return tecNO_PERMISSION;
// Clearing lsfMPTRequireAuth is invalid when the issuance already has
// a DomainID set, because a DomainID requires RequireAuth to be active.
if ((*mutableFlags & tmfMPTClearRequireAuth) != 0u &&
sleMptIssuance->isFieldPresent(sfDomainID))
return tecNO_PERMISSION;
}
if (!isMutableFlag(lsmfMPTCanMutateMetadata) && ctx.tx.isFieldPresent(sfMPTokenMetadata))

View File

@@ -0,0 +1,851 @@
#include <test/jtx/Env.h>
#include <test/jtx/envconfig.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/proto/org/xrpl/rpc/v1/get_ledger.pb.h>
#include <xrpl/proto/org/xrpl/rpc/v1/xrp_ledger.grpc.pb.h>
#include <boost/filesystem/operations.hpp>
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/grpcpp.h>
#include <grpcpp/security/credentials.h>
#include <grpcpp/support/status.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
namespace {
constexpr std::string_view kCA_CERT_CONTENT =
"-----BEGIN CERTIFICATE-----\n"
"MIIFhjCCA26gAwIBAgIJAL9P70zX30oiMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV\n"
"BAYTAlVTMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MRgwFgYDVQQKDA9S\n"
"aXBwbGVkIFRlc3QgQ0ExEDAOBgNVBAMMB1Rlc3QgQ0EwIBcNMjYwNDA5MTMyNTA2\n"
"WhgPMjEyNjAzMTYxMzI1MDZaMFcxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARUZXN0\n"
"MQ0wCwYDVQQHDARUZXN0MRgwFgYDVQQKDA9SaXBwbGVkIFRlc3QgQ0ExEDAOBgNV\n"
"BAMMB1Rlc3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzOJ5s\n"
"dy1O0GN/kmbeWf5DmFbQBSS9FRKxh6/o9V9BqRBQfECrVK9T5Y4FYrGGtmUW3YEV\n"
"uMDZ5q6rvBT2zrrzPXnWA5Pb4I4mKqC/yk5L7Mm8A9xQsNoRzgTyl/NuHiXKn+yQ\n"
"FuidA6U36qwIAcDR7gLqrJ1ud/ng9f9Q4k6+IItY/XGhcz4nKlQq9jpzmfdSlBkU\n"
"hXsIsdNtC+UGlQMCMX2jwysIFfjjCOMlH7KFQ3dKodhsW+Ym6AsPwyRGCgNXO/zd\n"
"Fqt1MIMs1F7r40DtfVO3R7w4/2SblcceZlsDrYQUbJnH+sEPWO0SGGo6Y7Ohs09+\n"
"aJSOAGGQVgTSLuAcFtR4BXD0GLn39+10PDvHGOsMJKL1de1f96s8kPlifQ5AGWuc\n"
"xy6XsupGSa0F8LozwQmKD7hVkyladUTWFPknz5tsPEVApTO0U8Vuknuhyovo6+mx\n"
"qBoSD32NwHveFz3jWqfj0CGX9BwL9AOpMabDhROVQfyM5GrLeLOOdgOnsBXJYYdW\n"
"MeJwz6BH30q9yvEd9Ti26jSk3fM8WPuEkZzNNp8STEMyDrfhaKOe5fGPWLnqMQAf\n"
"yMCDLwB1WqIN1Q6gOELb3rxyYDVH/5x6/JXosdUe1qx/tzvRoSWxxssRRd2Em+e+\n"
"MUFLXz+9D6kZ9XCuP/mLyRGW6LEiwwQkGKMnzwIDAQABo1MwUTAPBgNVHRMBAf8E\n"
"BTADAQH/MB0GA1UdDgQWBBQPK5hXxLdTj3QqfVzGpfTga6IF3zAfBgNVHSMEGDAW\n"
"gBQPK5hXxLdTj3QqfVzGpfTga6IF3zANBgkqhkiG9w0BAQsFAAOCAgEAa06whkqv\n"
"KmdT1HVhkV7AkWEAeHMWPLLaaFbcwble7a1Vizh6GjCyNpLtoN+mtwqwiOdsIlRE\n"
"42pWILc6CuuX0ae0nHSrcQS5mq8ZKSMr1xTo9RSfBq7CDfdyquxzG83HhpdApViZ\n"
"87Bjy3WoRuomM+YiONfUVdCbC5ZmXW/z+xrXJ+JqIXrtv66sZxpQIR0+ShnWT0DE\n"
"w9jB5fxjydPFwEudYi4z9XjEZaZJ1f8VNWDuUvi3yTJtTlNaWnKveudtDZBw/fA+\n"
"MBFd9ccYVhGQPxOs6S0Ev6q5IjcnzGeEBNZOjgjQk9aFrAs2Iiy018AbYQj5XD64\n"
"hHyiNgyPjl/VgXJE1Xl3lXGpiiJlXctgnCd3UGMfKznhBIpDT13i2CmHFyR3uk7o\n"
"UOZUXCnbnmgthejmFxB35Wf5TmGaYubtRMfCPHGNbQD+7Kg2+8eel3J3JSuG6RQ8\n"
"hwNyHHQnaPVUSANItJ4cMe5DutM0vUCMkJbajL+fjC5SdsTcGfR2VmAFqulNDXjH\n"
"sGWBiWVNsgddax63m6kL9UOeE+8pu8yStKZ4mVn2EjE9eJk4vyZt4BaI6sDUMlke\n"
"S9OjcI5iYlxXNgbRQBtwK70+c3D3JoRPREkTRPPwC4NiAFed7UwXSMh5nWbpt/dq\n"
"fAbAYqu0rfMFHUYjzIVnu8WRCC56qYHO5tU=\n"
"-----END CERTIFICATE-----\n";
constexpr std::string_view kSERVER_CERT_CONTENT =
"-----BEGIN CERTIFICATE-----\n"
"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\n"
"-----END CERTIFICATE-----\n";
constexpr std::string_view kSERVER_KEY_CONTENT =
"-----BEGIN PRIVATE KEY-----\n"
"MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCv+Lj9LJfPuSOE\n"
"yZqTn2gmG5tJt02ywnuIQet7N5tduxnNs50yXQ00Jeb40dth0HwI5I+AsEVNPIG3\n"
"7tJ9RtCwwTyltaZ4bXuL9ujEVr6TAKY6rlU9bL+Zmr62Lm8B0SLouxfPtyzKhBv6\n"
"7bGUrdIX7o9DbTQ3/2mQTc7KjPCJTEutmpVyD3dABN1qDM9Qzac0NtK1nixvYGd4\n"
"SpbK95BRXby3X9um0dVXoUMbc2gDV9uUZw1xLSjuKDJtQ/rleqe0mmS6JSoagwbb\n"
"DmPX/GbIf21IWUsg3m7AyIwYf9FtIJArB/j3iVBsY9lTB0mXSLxiyN6KM822+QjH\n"
"/VeHKDUWdQB6N3smmi1OLQasukRpSUTmTsoucn30dUqS6qdTtkHzvqGEN+CXBWgG\n"
"i1AS2CacOjYSVHRppC10r/3kEChYY/9rqYBz7GedFRJ6VzQzrwFYZleLJvX6GWfe\n"
"4gNvgwLfo/q2Af1HkCY2aHO+19eAghVsy1MRUDnm/GbZAhHSrX10iEfRjs+GhfxY\n"
"v0xMrvGBCm/2CiJ8RAvdRPpNkM/3u9fjOmqdKvE9NTqDOX1HUBoqa/UguIzi6o/k\n"
"BlBtohfaeL6ZeYXl6MefIIs2pipR7S1VQ1RY9OSdnN5nIJidyn1l85P9vLn49QVw\n"
"2OAT+TcEZnxyaiHCKU6nWtusuMt3wQIDAQABAoICAQCZilzm0uT3Y2RBdaMBUaKP\n"
"NaFONbl+00D0SAhOr9tJcnp2SFVN33Eo4jVhP8K62y2OmNc5gxRE6xmIQsK4enSW\n"
"9VSUhiXliCm3m03IGqQYIgXox7oqaVvYi/QBhAxpunBKPwzsubhET/cWABXlU7Ew\n"
"HoA0ZfGdNqeGOM3JYCZ0tfSGWo4xQptbaaND6D9wErDk1z0NKSE+YRCHHhXqrQ3o\n"
"YPDL08EVEpui5VtndU/5Msyt9Sj+alf/TWWKfzlIx7fS1rAy10Cgd1khA7JMf7ez\n"
"E7Rn3zm1ST+7yICs08IJBNOmKEOswMxCdvDmCELG1LlDPF8omUDSeQKXdU7M6GFA\n"
"b5PQ11Ik6xZVw1NUESf4d9g0VhEJRXSdGwA3KepAkwRejkB5jI56C8z9dB0LWdWH\n"
"2r3dX2ZpbJv0XVNxAELRgKwyfqWxYrF3caGLrxxWAiyPFvD9FgZJB1ftBU3D+HZZ\n"
"bltdfHJBgZe3pwoCr3X2JPhcA6ecITsset14dvsXHSi9IAXTHbeXxjrHCRcXs6xV\n"
"v4ZSL5r43dv6qk7XiFONCmV8diIwJOxcaSvoBgeeCykX4RKGSk/6Atlo4C9hXb47\n"
"BAuXu3Y+SkS98EljsdeNKCr013Tvt0p4H2QfeoDTKuzC+j3hu9fCkEP3oak2nWFl\n"
"bOkrYMJCc6yxu20G58vzrQKCAQEA1y93gNuNa7Z+VrZCSEcJX2BZl1mTyhLEa9mN\n"
"QOmKlW10VrfCsJxLu+dTGWccy0c6Q8wk6uGjgYJHsdyFPIdSroPR2ysJKSP/5Vzu\n"
"xNymgbeLPnWoivC9TctovWY/15fdboYNUO54jOpFheCC1wq9ZP6CyJmw5O96Y7tJ\n"
"1l5Dq7Fe4iQbIQHPt54wVVHsm7G1ZNywgSbt0HXHeP43YN3mRawJ51++MaEksCXv\n"
"rW+vOxPdiW8djE0tqcK0tqFMhI6p+WcUu8128aRHd0iHlKsVsFU4OLLZr10zwy9i\n"
"COHoF4Fh53pGp05jv+5eMtuEiem87ZUmpJn7whHZt8sKSE71AwKCAQEA0Vkwr4KA\n"
"kRRCUPvor5mdNil05N1mLrYgr/4UAHg3tbeTGxOjSX65KnJWi5dsDmZUdGTL4StD\n"
"8H6uLzzjX88gQkpKvtRYPYKBFtTRsI+ItOvIIo8czK/Kv8dwC2WXZbZBjsCAhrCm\n"
"0fKL2jx7rgdjaqvQeqSRtcHiyiYJG/jC7Iqwm4CyPr+nkVUWKZUWXopw0QXZXHWp\n"
"Glz9TXreEI7Xb/R+RXYU21exBqg0SfHq9pA//aNTQWxWGlNVwqO/KUao9HZupKHb\n"
"mA73oxFJTKhVNNNdC5cC91pxDeDTUzpIEjCGeLI3Aa35CD0WFqEbELJphr5HGkGo\n"
"VkYod6P79+Ta6wKCAQAadFpzvAop2Ni1XljNu/X6BMVe5wNVT3NYcvl7pnqEHl20\n"
"H4lO3xgsdKbxs4yFrS8LkLhlK/JHBLY9toemxlgy3j/ZevP4W9Wk5ATyrNHHlsIG\n"
"nr5mvmv3eW9aAY0Nuzzczpwqe/bUFCUR7WUIfOiF1whLEyH9MzfPtQHB2frly7uH\n"
"f7raFvfrcgYtJxI4neNYEA2fAyMvgptQU6iJPx6FKD5bdJjUTyRMh41svBNF5w5Q\n"
"TBnM2twnR6mh3jii/0sEP1j8MalS0ch7cK5CZ7oV4JQ13D8I4SNw9o1N3EAFS8G2\n"
"jIDNJsT6npp0FCq6LcMtTi3fBJM/66PhhZOxCgvzAoIBAH1LnE/vE3PBZE+D9afj\n"
"kKwx87xmphme98FdmCsPyIgB7xFtl3UNW1WESTgS0KFtrW5cRYnmkysFJssu7gcR\n"
"uIT0YfgErythSFGZ3kaGIZPm6kmEzf/T1s0hWHX5v7soceQ2YrY6VB2jxQBA4uUt\n"
"ltrpKkW86ViXSl0ilqEfKcrY1wq64/OaUXgyLKmGiXTb9tmjXoxv/12/+fq9ZtsS\n"
"Iu7mrgx0t9bvjQwm7+Sx3abkfugXMGUfqgjnh5SO3IKfv89QcrgmB3/itWPrnKs8\n"
"tIKBXlbpcuUIRFHCFbjiUPBSCqmCQFnI/htoNCgnFEPSBEaY64VTdqTsKJwykUO0\n"
"vTECggEAEAB8vyHHk7fpU+IOwD8TP7MCMHwoJzoHQp35So7TlhmO7oDranNhg3nl\n"
"jhTOeISLG2dmPkT49vhsO30tal4CgSXVZo1bPbOK83UvgeLH5Rhji44Dmah+ohKy\n"
"wCuVLuF6YSSp5rD7VIrahhegBFXEYdW5+ZBFbDpE5EXp0WeHc7IRPwWvm+ixr1m8\n"
"VqLeeh1xkMG5WdTTwGjgKWIFXZQ3bOIdVK7uya8wFDAtftkswXiBxAlb9L6Id+Dp\n"
"bKfMAHNouU1TQn5duFgPnCbSU1Js74HkkC0NEEIjQX8k2UCPrhV0VfLfViPuPFax\n"
"S/RYUSUkZ4VvqFUfo7wT8x18urb87w==\n"
"-----END PRIVATE KEY-----\n";
constexpr std::string_view kCLIENT_CERT_CONTENT =
"-----BEGIN CERTIFICATE-----\n"
"MIIFeDCCA2CgAwIBAgIJAIErcpMflkrSMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV\n"
"BAYTAlVTMQ0wCwYDVQQIDARUZXN0MQ0wCwYDVQQHDARUZXN0MRgwFgYDVQQKDA9S\n"
"aXBwbGVkIFRlc3QgQ0ExEDAOBgNVBAMMB1Rlc3QgQ0EwIBcNMjYwNDA5MTMyNTA3\n"
"WhgPMjEyNjAzMTYxMzI1MDdaMFoxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARUZXN0\n"
"MQ0wCwYDVQQHDARUZXN0MRcwFQYDVQQKDA5SaXBwbGVkIENsaWVudDEUMBIGA1UE\n"
"AwwLdGVzdC1jbGllbnQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP\n"
"QHttw3TLjOqYS3VkLF3KMRaP2ZtO6A1mXfTbqbKvD41Fazf/cM/v9lPMAlRd2SEY\n"
"3MeE8KVddKJwsbF0kNgDkKB5D5V42WrTw5biFNMOeHAZMR/oWChIvZbbGbDxIdIO\n"
"2+W3X0kjpa2eKcK9qBk8xoyIeilyXtleGWuWvHxiZP9iGHxaTWB+wIKUIK6vrEOb\n"
"iO3P/9XPpHzsBt0HdTDh4V7fwnr2UndVeQyBwUwLn6pd73sTKBfA26YppRwDjPIj\n"
"6NYtF3I28lRCUo+47TAVZM97gjN9oEwyHIVtOc7fnZPwtN26M5v5083SGXU1k/PN\n"
"3xGAlDUiCF3RSMbBRylGgUVtsAD57i8tI2SqCr+ZG233VFiOdwTRKKVNTyMC9TCJ\n"
"dCFFEDFDHTTSimKRQKy0Cm2qoL6JBkziAIiu/0Zzv9YjAAnRoJ2cweMXQ/9z1oWe\n"
"EUZBLRsggYQ8FbM13FOkOs8IlzacSuhwrYKOq8LsMX4cH2mnn783FtXXqrL/xfL7\n"
"11KhzGpZNrz187ilJ+ZsmP9D6vCBP/tR7V52dgtB6I291o8zxdH8GheIGenEFaZa\n"
"oAwyN2FuJgXZqx9319I9gYerZ/BbUzA2MuOxFd0ywtdcTPqKiyAQ9rxQVCVQyYWj\n"
"kfBEYRzWxjfj3XhNprxdm3cauz01NAoTDiz52dZhGQIDAQABo0IwQDAdBgNVHQ4E\n"
"FgQUXVKwiGRrXC1sjK2D86jsjMVV0XgwHwYDVR0jBBgwFoAUDyuYV8S3U490Kn1c\n"
"xqX04GuiBd8wDQYJKoZIhvcNAQELBQADggIBACpHTm9GZMZ7OPhqVo4VltVOW9a9\n"
"LLDsVYmvpAF9+yjZGims6+p3f7eY+o+TRdUE4HEBCmH0UiFVODXCZSoqXo6y9xq7\n"
"TS1dmXll1Sajbfi7YXsM8CAUb+cSsHtmT57JtbGicDiVXAqIOlT65yXkuujdcEa0\n"
"OAw45vJDkWk/6nneFJKdTs7aT3fvIGTlMAxgMJngVsA8BRsX8TWoo05Lum8ClNgi\n"
"s6mtl+nUvjOaM0omFL/K9kqLy7OJAbmE5xuhkC9q6Kn0pHBL4u0YSWaWTpyrvAX7\n"
"BuOE0G1JezcCAcqJvXbKFvhnOSHTvzdlMgXhteGW8Uwgf8cGKtVLSwh6YTjI1XaL\n"
"DkNZfJabAyH7BsGGbAd9Jts4h+4auPqHgcpEz16280oCgZdcfLSP0UKrfwYuXOar\n"
"8KWlVRFl2NBpEJwRf2KjZFQUqYoX1MmfX0gyy+kk0ZP12L7oGNqAxkaWySfb4PSv\n"
"Hsnb8iD6sIJQjZvZ/2wLV8xwFTbFjvGbmSx+XLnMUVV8cVAMUpZz5X2R9pBvpVi4\n"
"KfUccTvIVA0p1wFSdWYQ0+QNxHxZGX1rin6KVUdV1z8K6J3FgGlRqzfz4bruGpXs\n"
"6vX5vqF9KTFpwLTOxDU+kAoIfHowHeu/LQX1l+rk1ww2UZQ1zvgKb6fxWMtviq3F\n"
"cTe8jkzRqYdUfAoV\n"
"-----END CERTIFICATE-----\n";
constexpr std::string_view kCLIENT_KEY_CONTENT =
"-----BEGIN PRIVATE KEY-----\n"
"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\n"
"+6f4hsbp29hg+BUZ+UPdk7QyCKpZD1A=\n"
"-----END PRIVATE KEY-----\n";
/**
* RAII helper for managing temporary TLS certificates in tests.
*
* Creates a temporary directory and writes test certificates to it.
* Automatically cleans up the directory when destroyed.
*/
class TemporaryTLSCertificates
{
public:
static constexpr std::string_view kCA_CERT_FILENAME = "ca.pem";
static constexpr std::string_view kSERVER_CERT_FILENAME = "server_cert.pem";
static constexpr std::string_view kSERVER_KEY_FILENAME = "server_key.pem";
static constexpr std::string_view kCLIENT_CERT_FILENAME = "client_cert.pem";
static constexpr std::string_view kCLIENT_KEY_FILENAME = "client_key.pem";
static constexpr std::string_view kCERTS_DIR_PREFIX = "grpc_tls_test_";
TemporaryTLSCertificates()
{
auto tmpDir = std::filesystem::temp_directory_path();
auto uniqueDirName =
boost::filesystem::unique_path(std::string(kCERTS_DIR_PREFIX) + "%%%%%%%%");
tempDir_ = tmpDir / uniqueDirName.string();
std::filesystem::create_directories(tempDir_);
writeFile(tempDir_ / kCA_CERT_FILENAME, kCA_CERT_CONTENT);
writeFile(tempDir_ / kSERVER_CERT_FILENAME, kSERVER_CERT_CONTENT);
writeFile(tempDir_ / kSERVER_KEY_FILENAME, kSERVER_KEY_CONTENT);
writeFile(tempDir_ / kCLIENT_CERT_FILENAME, kCLIENT_CERT_CONTENT);
writeFile(tempDir_ / kCLIENT_KEY_FILENAME, kCLIENT_KEY_CONTENT);
}
virtual ~TemporaryTLSCertificates()
{
std::error_code ec;
std::filesystem::remove_all(tempDir_, ec);
}
TemporaryTLSCertificates(TemporaryTLSCertificates const&) = delete;
TemporaryTLSCertificates&
operator=(TemporaryTLSCertificates const&) = delete;
TemporaryTLSCertificates(TemporaryTLSCertificates&&) = delete;
TemporaryTLSCertificates&
operator=(TemporaryTLSCertificates&&) = delete;
[[nodiscard]] std::filesystem::path
getCACertPath() const
{
return tempDir_ / kCA_CERT_FILENAME;
}
[[nodiscard]] std::filesystem::path
getServerCertPath() const
{
return tempDir_ / kSERVER_CERT_FILENAME;
}
[[nodiscard]] std::filesystem::path
getServerKeyPath() const
{
return tempDir_ / kSERVER_KEY_FILENAME;
}
[[nodiscard]] std::filesystem::path
getClientCertPath() const
{
return tempDir_ / kCLIENT_CERT_FILENAME;
}
[[nodiscard]] std::filesystem::path
getClientKeyPath() const
{
return tempDir_ / kCLIENT_KEY_FILENAME;
}
[[nodiscard]] std::filesystem::path
getTempDir() const
{
return tempDir_;
}
private:
static void
writeFile(std::filesystem::path const& path, std::string_view content)
{
std::ofstream file(path);
if (!file)
throw std::runtime_error("Failed to create file: " + path.string());
file << content;
if (!file)
throw std::runtime_error("Failed to write file: " + path.string());
}
std::filesystem::path tempDir_;
};
} // namespace
namespace xrpl {
namespace test {
/**
* Helper function to make a simple gRPC call to test connectivity.
* Returns true if the call succeeded, false otherwise.
*/
bool
makeTestGRPCCall(std::unique_ptr<org::xrpl::rpc::v1::XRPLedgerAPIService::Stub> const& stub)
{
grpc::ClientContext context;
org::xrpl::rpc::v1::GetLedgerRequest const request;
org::xrpl::rpc::v1::GetLedgerResponse response;
// Set a short deadline to avoid hanging on failed connections
context.set_deadline(std::chrono::system_clock::now() + std::chrono::seconds(2));
grpc::Status const status = stub->GetLedger(&context, request, &response);
return status.ok();
}
class GRPCServerTLS_test : public beast::unit_test::suite, public TemporaryTLSCertificates
{
public:
void
testWithoutTLS()
{
testcase("GRPCServer without TLS");
using namespace jtx;
// Create config without TLS settings
auto cfg = envconfig(addGrpcConfig);
Env env(*this, std::move(cfg));
// Verify the server actually started by checking the port
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort > 0);
// Test 1: Plaintext client should connect successfully
std::string const serverAddress = "localhost:" + std::to_string(*grpcPort);
auto plaintextStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()));
BEAST_EXPECT(makeTestGRPCCall(plaintextStub));
}
void
testWithValidTLS()
{
testcase("GRPCServer with valid TLS configuration (no mutual TLS)");
using namespace jtx;
// Test with just server cert and key (no client verification)
auto cfg = envconfig(
addGrpcConfigWithTLS, getServerCertPath().string(), getServerKeyPath().string());
Env env(*this, std::move(cfg));
// Verify the server actually started by checking the port
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort > 0);
std::string const serverAddress = "localhost:" + std::to_string(*grpcPort);
// Test 1: Plaintext client should FAIL against TLS server
auto plaintextStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()));
BEAST_EXPECT(!makeTestGRPCCall(plaintextStub));
// Test 2: TLS client with server CA should succeed
grpc::SslCredentialsOptions sslOpts;
sslOpts.pem_root_certs = std::string(kCA_CERT_CONTENT);
auto tlsStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOpts)));
BEAST_EXPECT(makeTestGRPCCall(tlsStub));
}
void
testWithMutualTLS()
{
testcase("GRPCServer with mutual TLS (client verification enabled)");
using namespace jtx;
// Test with server cert, key, and CA for client verification
auto cfg = envconfig(
addGrpcConfigWithTLSAndClientCA,
getServerCertPath().string(),
getServerKeyPath().string(),
getCACertPath().string());
Env env(*this, std::move(cfg));
// Verify the server actually started by checking the port
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort > 0);
auto const serverAddress = "localhost:" + std::to_string(*grpcPort);
// Test 1: TLS client WITHOUT client certificate should FAIL (mTLS requires client cert)
grpc::SslCredentialsOptions sslOptsNoClient;
sslOptsNoClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
auto tlsStubNoClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsNoClient)));
BEAST_EXPECT(!makeTestGRPCCall(tlsStubNoClient));
// Test 2: TLS client WITH client certificate should succeed
grpc::SslCredentialsOptions sslOptsWithClient;
sslOptsWithClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
sslOptsWithClient.pem_cert_chain = std::string(kCLIENT_CERT_CONTENT);
sslOptsWithClient.pem_private_key = std::string(kCLIENT_KEY_CONTENT);
auto tlsStubWithClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsWithClient)));
BEAST_EXPECT(makeTestGRPCCall(tlsStubWithClient));
}
void
testWithMissingKey()
{
testcase("GRPCServer with cert but no key");
using namespace jtx;
// Create config with only cert (missing key)
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
// Intentionally omit ssl_key
try
{
Env const env(*this, std::move(cfg));
fail("Should have thrown exception for incomplete TLS config");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("Incomplete TLS configuration") != std::string::npos);
}
}
void
testWithMissingCert()
{
testcase("GRPCServer with key but no cert");
using namespace jtx;
// Create config with only key (missing cert)
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
// Intentionally omit ssl_cert
try
{
Env const env(*this, std::move(cfg));
fail("Should have thrown exception for incomplete TLS config");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("Incomplete TLS configuration") != std::string::npos);
}
}
void
testWithClientCAButNoTLS()
{
testcase("GRPCServer with ssl_client_ca but without both ssl_cert and ssl_key");
using namespace jtx;
// Test 1: ssl_client_ca specified without any TLS config
{
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
// Intentionally omit both ssl_cert and ssl_key
try
{
Env const env(*this, std::move(cfg));
fail("Should have thrown exception for ssl_client_ca without TLS config");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find(
"ssl_client_ca requires both ssl_cert and ssl_key") != std::string::npos);
}
}
// Test 2: ssl_client_ca with only ssl_cert (missing ssl_key)
{
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
// Intentionally omit ssl_key
try
{
Env const env(*this, std::move(cfg));
fail("Should have thrown exception for ssl_client_ca with only ssl_cert");
}
catch (std::runtime_error const& e)
{
// This should fail with "Incomplete TLS configuration" first
// because ssl_cert is specified without ssl_key
BEAST_EXPECT(
std::string(e.what()).find("Incomplete TLS configuration") !=
std::string::npos);
}
}
// Test 3: ssl_client_ca with only ssl_key (missing ssl_cert)
{
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
// Intentionally omit ssl_cert
try
{
Env const env(*this, std::move(cfg));
fail("Should have thrown exception for ssl_client_ca with only ssl_key");
}
catch (std::runtime_error const& e)
{
// This should fail with "Incomplete TLS configuration" first
// because ssl_key is specified without ssl_cert
BEAST_EXPECT(
std::string(e.what()).find("Incomplete TLS configuration") !=
std::string::npos);
}
}
}
void
testWithCertChainButNoTLS()
{
testcase("GRPCServer with ssl_cert_chain but without both ssl_cert and ssl_key");
using namespace jtx;
// Test 1: ssl_cert_chain specified without any TLS config
{
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", getCACertPath().string());
// Intentionally omit both ssl_cert and ssl_key
try
{
Env const env(*this, std::move(cfg));
fail("Should have thrown exception for ssl_cert_chain without TLS config");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find(
"ssl_cert_chain requires both ssl_cert and ssl_key") != std::string::npos);
}
}
// Test 2: ssl_cert_chain with only ssl_cert (missing ssl_key)
{
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", getCACertPath().string());
// Intentionally omit ssl_key
try
{
Env const env(*this, std::move(cfg));
fail("Should have thrown exception for ssl_cert_chain with only ssl_cert");
}
catch (std::runtime_error const& e)
{
// This should fail with "Incomplete TLS configuration" first
// because ssl_cert is specified without ssl_key
BEAST_EXPECT(
std::string(e.what()).find("Incomplete TLS configuration") !=
std::string::npos);
}
}
}
void
testWithCertChain()
{
testcase("GRPCServer with ssl_cert_chain for intermediate CA certificates");
using namespace jtx;
// Test with server cert, key, and cert chain (intermediate CA)
// In this test, we use the CA cert as a stand-in for an intermediate CA cert
auto cfg = envconfig(
addGrpcConfigWithTLSAndCertChain,
getServerCertPath().string(),
getServerKeyPath().string(),
getCACertPath().string());
Env env(*this, std::move(cfg));
// Verify the server actually started by checking the port
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort > 0);
auto const serverAddress = "localhost:" + std::to_string(*grpcPort);
// Test: TLS client should be able to connect (no client cert required)
grpc::SslCredentialsOptions sslOpts;
sslOpts.pem_root_certs = std::string(kCA_CERT_CONTENT);
auto tlsStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOpts)));
BEAST_EXPECT(makeTestGRPCCall(tlsStub));
// Insecure client should fail
auto insecureStub = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials()));
BEAST_EXPECT(!makeTestGRPCCall(insecureStub));
}
void
testWithInvalidCertFile()
{
testcase("GRPCServer with invalid/non-existent certificate file");
using namespace jtx;
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", "/nonexistent/path/to/cert.pem");
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
Env env(*this, std::move(cfg));
// Server should fail to start - verify port is 0
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
}
void
testWithInvalidKeyFile()
{
testcase("GRPCServer with invalid/non-existent key file");
using namespace jtx;
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", "/nonexistent/path/to/key.pem");
Env env(*this, std::move(cfg));
// Server should fail to start - verify port is 0
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
}
void
testWithInvalidCertChainFile()
{
testcase("GRPCServer with invalid/non-existent cert chain file");
using namespace jtx;
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", "/nonexistent/path/to/chain.pem");
Env env(*this, std::move(cfg));
// Server should fail to start - verify port is 0
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
}
void
testWithInvalidClientCAFile()
{
testcase("GRPCServer with invalid/non-existent client CA file");
using namespace jtx;
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", "/nonexistent/path/to/ca.pem");
Env env(*this, std::move(cfg));
// Server should fail to start - verify port is 0
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
}
void
testWithEmptyClientCAFile()
{
testcase("GRPCServer with empty client CA file");
using namespace jtx;
// Create an empty file for client CA
auto emptyCAPath = getTempDir() / "empty_ca.pem";
std::ofstream emptyFile(emptyCAPath);
emptyFile.close();
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", "127.0.0.1");
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", emptyCAPath.string());
Env env(*this, std::move(cfg));
// Server should fail to start due to empty CA file
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort == 0); // Server should not have started
}
void
testWithBothCertChainAndClientCA()
{
testcase("GRPCServer with both cert chain and client CA (full mTLS with intermediates)");
using namespace jtx;
// Test with all TLS features enabled: cert, key, cert_chain, and client_ca
auto cfg = envconfig();
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", getServerCertPath().string());
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", getServerKeyPath().string());
(*cfg)[SECTION_PORT_GRPC].set(
"ssl_cert_chain", getCACertPath().string()); // Using CA as intermediate
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", getCACertPath().string());
Env env(*this, std::move(cfg));
// Verify the server started successfully
auto const grpcPort = env.app().config()[SECTION_PORT_GRPC].get<unsigned int>("port");
BEAST_EXPECT(grpcPort.has_value());
BEAST_EXPECT(*grpcPort > 0);
auto const serverAddress = "localhost:" + std::to_string(*grpcPort);
// Test 1: TLS client WITHOUT client certificate should FAIL (mTLS requires client cert)
grpc::SslCredentialsOptions sslOptsNoClient;
sslOptsNoClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
auto tlsStubNoClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsNoClient)));
BEAST_EXPECT(!makeTestGRPCCall(tlsStubNoClient));
// Test 2: TLS client WITH client certificate should succeed
grpc::SslCredentialsOptions sslOptsWithClient;
sslOptsWithClient.pem_root_certs = std::string(kCA_CERT_CONTENT);
sslOptsWithClient.pem_cert_chain = std::string(kCLIENT_CERT_CONTENT);
sslOptsWithClient.pem_private_key = std::string(kCLIENT_KEY_CONTENT);
auto tlsStubWithClient = org::xrpl::rpc::v1::XRPLedgerAPIService::NewStub(
grpc::CreateChannel(serverAddress, grpc::SslCredentials(sslOptsWithClient)));
BEAST_EXPECT(makeTestGRPCCall(tlsStubWithClient));
}
void
run() override
{
testWithoutTLS();
testWithValidTLS();
testWithMutualTLS();
testWithMissingKey();
testWithMissingCert();
testWithClientCAButNoTLS();
testWithCertChainButNoTLS();
testWithCertChain();
testWithInvalidCertFile();
testWithInvalidKeyFile();
testWithInvalidCertChainFile();
testWithInvalidClientCAFile();
testWithEmptyClientCAFile();
testWithBothCertChainAndClientCA();
}
};
BEAST_DEFINE_TESTSUITE(GRPCServerTLS, app, xrpl);
} // namespace test
} // namespace xrpl

View File

@@ -1774,8 +1774,10 @@ class Invariants_test : public beast::unit_test::suite
using namespace test::jtx;
bool const fixPDEnabled = features[fixPermissionedDomainInvariant];
bool const fixS313Enabled = features[fixSecurity3_1_3];
testcase << "PermissionedDEX" + std::string(fixPDEnabled ? " fix" : "");
testcase << "PermissionedDEX" + std::string(fixPDEnabled ? " fixPD" : "") +
std::string(fixS313Enabled ? " fixS313" : "");
doInvariantCheck(
Env(*this, features),
@@ -1863,6 +1865,45 @@ class Invariants_test : public beast::unit_test::suite
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
}
// empty sfAdditionalBooks (size 0)
{
Env env1(*this, features);
Account const A1{"A1"};
Account const A2{"A2"};
env1.fund(XRP(1000), A1, A2);
env1.close();
[[maybe_unused]] auto [seq1, pd1] = createPermissionedDomainEnv(env1, A1, A2);
env1.close();
doInvariantCheck(
std::move(env1),
A1,
A2,
fixS313Enabled ? std::vector<std::string>{{"hybrid offer is malformed"}}
: std::vector<std::string>{},
[&pd1](Account const& A1, Account const& A2, ApplyContext& ac) {
Keylet const offerKey = keylet::offer(A2.id(), 10);
auto sleOffer = std::make_shared<SLE>(offerKey);
sleOffer->setAccountID(sfAccount, A2);
sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10));
sleOffer->setFieldAmount(sfTakerGets, XRP(1));
sleOffer->setFlag(lsfHybrid);
sleOffer->setFieldH256(sfDomainID, pd1);
STArray const bookArr; // empty array, size 0
sleOffer->setFieldArray(sfAdditionalBooks, bookArr);
ac.view().insert(sleOffer);
return true;
},
XRPAmount{},
STTx{ttOFFER_CREATE, [&](STObject&) {}},
fixS313Enabled
? std::initializer_list<TER>{tecINVARIANT_FAILED, tecINVARIANT_FAILED}
: std::initializer_list<TER>{tesSUCCESS, tesSUCCESS});
}
// hybrid offer missing sfAdditionalBooks
{
Env env1(*this, features);
@@ -4061,6 +4102,10 @@ public:
testPermissionedDomainInvariants(defaultAmendments() - fixPermissionedDomainInvariant);
testPermissionedDEX(defaultAmendments() | fixPermissionedDomainInvariant);
testPermissionedDEX(defaultAmendments() - fixPermissionedDomainInvariant);
testPermissionedDEX(
(defaultAmendments() | fixPermissionedDomainInvariant) - fixSecurity3_1_3);
testPermissionedDEX(
defaultAmendments() - fixPermissionedDomainInvariant - fixSecurity3_1_3);
testNoModifiedUnmodifiableFields();
testValidPseudoAccounts();
testValidLoanBroker();

View File

@@ -5,17 +5,21 @@
#include <test/jtx/noop.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/misc/SHAMapStore.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/ToString.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <cstdint>
#include <memory>
#include <sstream>
#include <vector>
namespace xrpl {
@@ -112,6 +116,73 @@ class LedgerMaster_test : public beast::unit_test::suite
}
}
void
testCompleteLedgerRange(FeatureBitset features)
{
// Note that this test is intentionally very similar to
// SHAMapStore_test::testLedgerGaps, but has a different
// focus.
testcase("Complete Ledger operations");
using namespace test::jtx;
auto const deleteInterval = 8;
Env env{*this, envconfig([](auto cfg) {
return online_delete(std::move(cfg), deleteInterval);
})};
auto const alice = Account("alice");
env.fund(XRP(1000), alice);
env.close();
auto& lm = env.app().getLedgerMaster();
LedgerIndex minSeq = 2;
LedgerIndex maxSeq = env.closed()->header().seq;
auto& store = env.app().getSHAMapStore();
store.rendezvous();
LedgerIndex lastRotated = store.getLastRotated();
BEAST_EXPECTS(maxSeq == 3, to_string(maxSeq));
BEAST_EXPECTS(lm.getCompleteLedgers() == "2-3", lm.getCompleteLedgers());
BEAST_EXPECTS(lastRotated == 3, to_string(lastRotated));
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
// Close enough ledgers to rotate a few times
for (int i = 0; i < 24; ++i)
{
for (int t = 0; t < 3; ++t)
{
env(noop(alice));
}
env.close();
store.rendezvous();
++maxSeq;
if (maxSeq == lastRotated + deleteInterval)
{
minSeq = lastRotated;
lastRotated = maxSeq;
}
BEAST_EXPECTS(
env.closed()->header().seq == maxSeq, to_string(env.closed()->header().seq));
BEAST_EXPECTS(store.getLastRotated() == lastRotated, to_string(store.getLastRotated()));
std::stringstream expectedRange;
expectedRange << minSeq << "-" << maxSeq;
BEAST_EXPECTS(lm.getCompleteLedgers() == expectedRange.str(), lm.getCompleteLedgers());
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
}
}
public:
void
run() override
@@ -125,6 +196,7 @@ public:
testWithFeats(FeatureBitset features)
{
testTxnIdFromIndex(features);
testCompleteLedgerRange(features);
}
};

View File

@@ -3042,45 +3042,83 @@ class MPToken_test : public beast::unit_test::suite
testcase("Mutate MPTRequireAuth");
using namespace test::jtx;
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
// test mutating RequireAuth flag on the issuance and its effect on payment authorization
{
Env env{*this, features};
Account const alice("alice");
Account const bob("bob");
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.flags = tfMPTRequireAuth,
.mutableFlags = tmfMPTCanMutateRequireAuth});
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create(
{.ownerCount = 1,
.flags = tfMPTRequireAuth,
.mutableFlags = tmfMPTCanMutateRequireAuth});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = alice, .holder = bob});
mptAlice.authorize({.account = bob});
mptAlice.authorize({.account = alice, .holder = bob});
// Pay to bob
mptAlice.pay(alice, bob, 1000);
// Pay to bob
mptAlice.pay(alice, bob, 1000);
// Unauthorize bob
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
// Unauthorize bob
mptAlice.authorize({.account = alice, .holder = bob, .flags = tfMPTUnauthorize});
// Can not pay to bob
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
// Can not pay to bob
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
// Clear RequireAuth
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearRequireAuth});
// Clear RequireAuth
mptAlice.set({.account = alice, .mutableFlags = tmfMPTClearRequireAuth});
// Can pay to bob
mptAlice.pay(alice, bob, 1000);
// Can pay to bob
mptAlice.pay(alice, bob, 1000);
// Set RequireAuth again
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
// Set RequireAuth again
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
// Can not pay to bob since he is not authorized
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
// Can not pay to bob since he is not authorized
mptAlice.pay(bob, alice, 100, tecNO_AUTH);
// Authorize bob again
mptAlice.authorize({.account = alice, .holder = bob});
// Authorize bob again
mptAlice.authorize({.account = alice, .holder = bob});
// Can pay to bob again
mptAlice.pay(alice, bob, 100);
// Can pay to bob again
mptAlice.pay(alice, bob, 100);
}
// Cannot clear RequireAuth when a DomainID is set on the issuance
{
Account const alice{"alice"};
Account const bob{"bob"};
Account const credIssuer{"credIssuer"};
pdomain::Credentials const credentials{
{.issuer = credIssuer, .credType = "credential"}};
Env env{*this, features};
env.fund(XRP(1000), credIssuer);
env.close();
env(pdomain::setTx(credIssuer, credentials));
env.close();
auto const domainId = pdomain::getNewDomain(env.meta());
MPTTester mptAlice(env, alice, {.holders = {bob}});
mptAlice.create({
.ownerCount = 1,
.flags = tfMPTRequireAuth,
.mutableFlags = tmfMPTCanMutateRequireAuth,
.domainID = domainId,
});
// Clearing RequireAuth while a DomainID is present must be rejected,
mptAlice.set({
.account = alice,
.mutableFlags = tmfMPTClearRequireAuth,
.err = tecNO_PERMISSION,
});
// Setting RequireAuth (already set) is still allowed, though it has no effect.
mptAlice.set({.account = alice, .mutableFlags = tmfMPTSetRequireAuth});
}
}
void

View File

@@ -20,6 +20,8 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/ledger/OpenView.h>
#include <xrpl/protocol/Book.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -28,6 +30,8 @@
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
@@ -35,6 +39,7 @@
#include <cstddef>
#include <cstdint>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
@@ -1385,6 +1390,73 @@ class PermissionedDEX_test : public beast::unit_test::suite
BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq));
}
void
testHybridMalformedOffer(FeatureBitset features)
{
bool const fixS313Enabled = features[fixSecurity3_1_3];
testcase << "Hybrid offer with empty AdditionalBooks"
<< (fixS313Enabled ? " (fixSecurity3_1_3 enabled)"
: " (fixSecurity3_1_3 disabled)");
// offerInDomain has two code paths gated by fixSecurity3_1_3:
//
// pre-fix: only rejects a hybrid offer when sfAdditionalBooks is
// entirely absent — an empty array (size 0) passes through.
// post-fix: also rejects a hybrid offer whose sfAdditionalBooks array
// has size != 1 (i.e. 0 or >1 entries).
//
// We create a valid hybrid offer, then directly manipulate its SLE to
// produce the size==0 case that cannot occur via normal transactions,
// and verify that the two code paths produce the expected outcomes.
//
// Note: the PermissionedDEX invariant checker (ValidPermissionedDEX)
// does not flag this malformation for ttPAYMENT — only for
// ttOFFER_CREATE — so the without-fix payment completes as tesSUCCESS.
Env env(*this, features);
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
PermissionedDEX(env);
// Create a valid hybrid offer (sfAdditionalBooks has exactly 1 entry)
auto const bobOfferSeq{env.seq(bob)};
env(offer(bob, XRP(10), USD(10)), txflags(tfHybrid), domain(domainID));
env.close();
BEAST_EXPECT(offerExists(env, bob, bobOfferSeq));
// Directly manipulate the offer SLE in the open ledger so that
// sfAdditionalBooks is present but empty (size 0). This is the
// malformed state that fixSecurity3_1_3 is designed to catch.
auto const offerKey = keylet::offer(bob.id(), bobOfferSeq);
env.app().getOpenLedger().modify([&offerKey](OpenView& view, beast::Journal) {
auto const sle = view.read(offerKey);
if (!sle)
return false;
auto replacement = std::make_shared<SLE>(*sle, sle->key());
replacement->setFieldArray(sfAdditionalBooks, STArray{});
view.rawReplace(replacement);
return true;
});
if (fixS313Enabled)
{
// post-fixSecurity3_1_3: offerInDomain rejects the malformed
// offer (size == 0), so no valid domain offer is found.
env(pay(alice, carol, USD(10)),
path(~USD),
sendmax(XRP(10)),
domain(domainID),
ter(tecPATH_PARTIAL));
}
else
{
// pre-fixSecurity3_1_3: offerInDomain only checks for a missing
// sfAdditionalBooks field; size == 0 passes through, so the
// malformed offer is crossed and the payment succeeds.
env(pay(alice, carol, USD(10)), path(~USD), sendmax(XRP(10)), domain(domainID));
}
}
public:
void
run() override
@@ -1406,6 +1478,8 @@ public:
testHybridBookStep(all);
testHybridInvalidOffer(all);
testHybridOfferDirectories(all);
testHybridMalformedOffer(all);
testHybridMalformedOffer(all - fixSecurity3_1_3);
}
};

View File

@@ -1,7 +1,9 @@
#include <test/jtx/Env.h>
#include <test/jtx/amount.h>
#include <test/jtx/envconfig.h>
#include <test/jtx/noop.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/main/NodeStoreScheduler.h>
#include <xrpld/app/misc/SHAMapStore.h>
@@ -29,7 +31,9 @@
#include <map>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include <utility>
namespace xrpl {
@@ -42,10 +46,8 @@ class SHAMapStore_test : public beast::unit_test::suite
static auto
onlineDelete(std::unique_ptr<Config> cfg)
{
cfg->LEDGER_HISTORY = deleteInterval;
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("online_delete", std::to_string(deleteInterval));
return cfg;
using namespace jtx;
return online_delete(std::move(cfg), deleteInterval);
}
static auto
@@ -585,6 +587,160 @@ public:
BEAST_EXPECT(dbr->getName() == "3");
}
void
testLedgerGaps()
{
// Note that this test is intentionally very similar to
// LedgerMaster_test::testCompleteLedgerRange, but has a different
// focus.
testcase("Wait for ledger gaps to fill in");
using namespace test::jtx;
Env env{*this, envconfig(onlineDelete)};
auto failureMessage = [&](char const* label, auto expected, auto actual) {
std::stringstream ss;
ss << label << ": Expected: " << expected << ", Got: " << actual;
return ss.str();
};
auto const alice = Account("alice");
env.fund(XRP(1000), alice);
env.close();
auto& lm = env.app().getLedgerMaster();
LedgerIndex minSeq = 2;
LedgerIndex maxSeq = env.closed()->header().seq;
auto& store = env.app().getSHAMapStore();
store.rendezvous();
LedgerIndex lastRotated = store.getLastRotated();
BEAST_EXPECTS(maxSeq == 3, to_string(maxSeq));
BEAST_EXPECTS(lm.getCompleteLedgers() == "2-3", lm.getCompleteLedgers());
BEAST_EXPECTS(lastRotated == 3, to_string(lastRotated));
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
// Close enough ledgers to rotate a few times
while (maxSeq < 20)
{
for (int t = 0; t < 3; ++t)
{
env(noop(alice));
}
env.close();
store.rendezvous();
++maxSeq;
if (maxSeq + 1 == lastRotated + deleteInterval)
{
using namespace std::chrono_literals;
// The next ledger will trigger a rotation. Delete the
// current ledger from LedgerMaster.
std::this_thread::sleep_for(100ms);
LedgerIndex const deleteSeq = maxSeq;
while (!lm.haveLedger(deleteSeq))
{
std::this_thread::sleep_for(100ms);
}
lm.clearLedger(deleteSeq);
auto expectedRange = [](auto minSeq, auto deleteSeq, auto maxSeq) {
std::stringstream expectedRange;
expectedRange << minSeq << "-" << (deleteSeq - 1);
if (deleteSeq + 1 == maxSeq)
{
expectedRange << "," << maxSeq;
}
else if (deleteSeq < maxSeq)
{
expectedRange << "," << (deleteSeq + 1) << "-" << maxSeq;
}
return expectedRange.str();
};
BEAST_EXPECTS(
lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeq, maxSeq),
failureMessage(
"Complete ledgers",
expectedRange(minSeq, deleteSeq, maxSeq),
lm.getCompleteLedgers()));
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 1);
// Close another ledger, which will trigger a rotation, but the
// rotation will be stuck until the missing ledger is filled in.
env.close();
// DO NOT CALL rendezvous()! You'll end up with a deadlock.
++maxSeq;
// Nothing has changed
BEAST_EXPECTS(
store.getLastRotated() == lastRotated,
failureMessage("lastRotated", lastRotated, store.getLastRotated()));
BEAST_EXPECTS(
lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeq, maxSeq),
failureMessage(
"Complete ledgers",
expectedRange(minSeq, deleteSeq, maxSeq),
lm.getCompleteLedgers()));
// Close 5 more ledgers, waiting one second in between to
// simulate the ledger making progress while online delete waits
// for the missing ledger to be filled in.
// This ensures the healthWait check has time to run and
// detect the gap.
for (int l = 0; l < 5; ++l)
{
env.close();
// DO NOT CALL rendezvous()! You'll end up with a deadlock.
++maxSeq;
// Nothing has changed
BEAST_EXPECTS(
store.getLastRotated() == lastRotated,
failureMessage("lastRotated", lastRotated, store.getLastRotated()));
BEAST_EXPECTS(
lm.getCompleteLedgers() == expectedRange(minSeq, deleteSeq, maxSeq),
failureMessage(
"Complete Ledgers",
expectedRange(minSeq, deleteSeq, maxSeq),
lm.getCompleteLedgers()));
std::this_thread::sleep_for(1s);
}
// Put the missing ledger back in LedgerMaster
lm.setLedgerRangePresent(deleteSeq, deleteSeq);
// Wait for the rotation to finish
store.rendezvous();
minSeq = lastRotated;
lastRotated = deleteSeq + 1;
}
BEAST_EXPECT(maxSeq != lastRotated + deleteInterval);
BEAST_EXPECTS(
env.closed()->header().seq == maxSeq,
failureMessage("maxSeq", maxSeq, env.closed()->header().seq));
BEAST_EXPECTS(
store.getLastRotated() == lastRotated,
failureMessage("lastRotated", lastRotated, store.getLastRotated()));
std::stringstream expectedRange;
expectedRange << minSeq << "-" << maxSeq;
BEAST_EXPECTS(
lm.getCompleteLedgers() == expectedRange.str(),
failureMessage("CompleteLedgers", expectedRange.str(), lm.getCompleteLedgers()));
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq, maxSeq) == 0);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 1, maxSeq - 1) == 0);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 1, maxSeq + 1) == 2);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq - 2, maxSeq - 2) == 2);
BEAST_EXPECT(lm.missingFromCompleteLedgerRange(minSeq + 2, maxSeq + 2) == 2);
}
}
void
run() override
{
@@ -592,6 +748,7 @@ public:
testAutomatic();
testCanDelete();
testRotate();
testLedgerGaps();
}
};

View File

@@ -57,6 +57,17 @@ envconfig(F&& modfunc, Args&&... args)
return modfunc(envconfig(), std::forward<Args>(args)...);
}
/// @brief adjust config to enable online_delete
///
/// @param cfg config instance to be modified
///
/// @param deleteInterval how many new ledgers should be available before
/// rotating. Defaults to 8, because the standalone minimum is 8.
///
/// @return unique_ptr to Config instance
std::unique_ptr<Config>
online_delete(std::unique_ptr<Config> cfg, std::uint32_t deleteInterval = 8);
/// @brief adjust config so no admin ports are enabled
///
/// this is intended for use with envconfig, as in
@@ -107,6 +118,52 @@ std::unique_ptr<Config> addGrpcConfig(std::unique_ptr<Config>);
std::unique_ptr<Config>
addGrpcConfigWithSecureGateway(std::unique_ptr<Config>, std::string const& secureGateway);
/// @brief add a grpc address, port and TLS certificate/key paths to config
///
/// This is intended for use with envconfig, for tests that require a grpc
/// server with TLS enabled.
///
/// @param cfg config instance to be modified
/// @param certPath path to SSL certificate file
/// @param keyPath path to SSL private key file
std::unique_ptr<Config>
addGrpcConfigWithTLS(
std::unique_ptr<Config>,
std::string const& certPath,
std::string const& keyPath);
/// @brief add a grpc address, port and TLS certificate/key/client CA paths to config
///
/// This is intended for use with envconfig, for tests that require a grpc
/// server with mutual TLS (client certificate verification) enabled.
///
/// @param cfg config instance to be modified
/// @param certPath path to SSL certificate file
/// @param keyPath path to SSL private key file
/// @param clientCAPath path to SSL client CA certificate file for mTLS
std::unique_ptr<Config>
addGrpcConfigWithTLSAndClientCA(
std::unique_ptr<Config>,
std::string const& certPath,
std::string const& keyPath,
std::string const& clientCAPath);
/// @brief add a grpc address, port and TLS with server cert chain to config
///
/// This is intended for use with envconfig, for tests that require a grpc
/// server with TLS enabled and intermediate CA certificates.
///
/// @param cfg config instance to be modified
/// @param certPath path to SSL certificate file
/// @param keyPath path to SSL private key file
/// @param certChainPath path to SSL intermediate CA certificate(s) file
std::unique_ptr<Config>
addGrpcConfigWithTLSAndCertChain(
std::unique_ptr<Config>,
std::string const& certPath,
std::string const& keyPath,
std::string const& certChainPath);
std::unique_ptr<Config>
makeConfig(
std::map<std::string, std::string> extraTxQ = {},

View File

@@ -48,7 +48,7 @@ class JSONRPCClient : public AbstractClient
continue;
ParsedPort pp;
parse_Port(pp, cfg[name], log);
if (pp.protocol.count("http") == 0)
if (not pp.protocol.contains("http"))
continue;
using namespace boost::asio::ip;
if (pp.ip && pp.ip->is_unspecified())
@@ -91,12 +91,6 @@ public:
stream_.connect(ep_);
}
~JSONRPCClient() override
{
// stream_.shutdown(boost::asio::ip::tcp::socket::shutdown_both);
// stream_.close();
}
/*
Return value is an Object type with up to three keys:
status

View File

@@ -6,8 +6,10 @@
#include <xrpld/core/ConfigSections.h>
#include <atomic>
#include <cstdint>
#include <map>
#include <memory>
#include <string>
#include <vector>
namespace xrpl {
@@ -60,6 +62,15 @@ setupConfigForUnitTests(Config& cfg)
namespace jtx {
std::unique_ptr<Config>
online_delete(std::unique_ptr<Config> cfg, std::uint32_t deleteInterval)
{
cfg->LEDGER_HISTORY = deleteInterval;
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("online_delete", std::to_string(deleteInterval));
return cfg;
}
std::unique_ptr<Config>
no_admin(std::unique_ptr<Config> cfg)
{
@@ -132,6 +143,49 @@ addGrpcConfigWithSecureGateway(std::unique_ptr<Config> cfg, std::string const& s
return cfg;
}
std::unique_ptr<Config>
addGrpcConfigWithTLS(
std::unique_ptr<Config> cfg,
std::string const& certPath,
std::string const& keyPath)
{
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", certPath);
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", keyPath);
return cfg;
}
std::unique_ptr<Config>
addGrpcConfigWithTLSAndClientCA(
std::unique_ptr<Config> cfg,
std::string const& certPath,
std::string const& keyPath,
std::string const& clientCAPath)
{
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", certPath);
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", keyPath);
(*cfg)[SECTION_PORT_GRPC].set("ssl_client_ca", clientCAPath);
return cfg;
}
std::unique_ptr<Config>
addGrpcConfigWithTLSAndCertChain(
std::unique_ptr<Config> cfg,
std::string const& certPath,
std::string const& keyPath,
std::string const& certChainPath)
{
(*cfg)[SECTION_PORT_GRPC].set("ip", getEnvLocalhostAddr());
(*cfg)[SECTION_PORT_GRPC].set("port", "0");
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert", certPath);
(*cfg)[SECTION_PORT_GRPC].set("ssl_key", keyPath);
(*cfg)[SECTION_PORT_GRPC].set("ssl_cert_chain", certChainPath);
return cfg;
}
std::unique_ptr<Config>
makeConfig(
std::map<std::string, std::string> extraTxQ,

View File

@@ -105,7 +105,10 @@ public:
failedSave(std::uint32_t seq, uint256 const& hash);
std::string
getCompleteLedgers();
getCompleteLedgers() const;
std::size_t
missingFromCompleteLedgerRange(LedgerIndex first, LedgerIndex last) const;
/** Apply held transactions to the open ledger
This is normally called as we close the ledger.
@@ -322,7 +325,7 @@ private:
// A set of transactions to replay during the next close
std::unique_ptr<LedgerReplay> replayData;
std::recursive_mutex mCompleteLock;
std::recursive_mutex mutable mCompleteLock;
RangeSet<std::uint32_t> mCompleteLedgers;
// Publish thread is running.

View File

@@ -1570,12 +1570,34 @@ LedgerMaster::getPublishedLedger()
}
std::string
LedgerMaster::getCompleteLedgers()
LedgerMaster::getCompleteLedgers() const
{
std::lock_guard const sl(mCompleteLock);
return to_string(mCompleteLedgers);
}
std::size_t
LedgerMaster::missingFromCompleteLedgerRange(LedgerIndex first, LedgerIndex last) const
{
// Make a copy of the range to avoid holding the lock
auto const range = [&] {
std::lock_guard const sl(mCompleteLock);
return mCompleteLedgers;
}();
std::size_t missing = 0;
for (LedgerIndex idx = first; idx <= last; ++idx)
{
if (!boost::icl::contains(range, idx))
{
++missing;
}
}
return missing;
}
std::optional<NetClock::time_point>
LedgerMaster::getCloseTimeBySeq(LedgerIndex ledgerIndex)
{

View File

@@ -8,6 +8,7 @@
#include <xrpld/rpc/detail/Handler.h>
#include <xrpl/basics/BasicConfig.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/contract.h>
#include <xrpl/beast/core/CurrentThreadName.h>
@@ -27,6 +28,7 @@
#include <boost/asio/ip/tcp.hpp>
#include <boost/icl/interval_set.hpp>
#include <grpc/grpc_security_constants.h>
#include <grpcpp/completion_queue.h>
#include <grpcpp/security/server_credentials.h>
#include <grpcpp/server_builder.h>
@@ -387,6 +389,48 @@ GRPCServerImpl::GRPCServerImpl(Application& app)
Throw<std::runtime_error>("Error parsing secure_gateway section");
}
}
// Read TLS certificate configuration (optional)
sslCertPath_ = section.get("ssl_cert");
sslKeyPath_ = section.get("ssl_key");
sslCertChainPath_ = section.get("ssl_cert_chain");
sslClientCAPath_ = section.get("ssl_client_ca");
// If cert or key is specified, both must be specified
if (sslCertPath_.has_value() || sslKeyPath_.has_value())
{
if (!sslCertPath_.has_value() || !sslKeyPath_.has_value())
{
JLOG(journal_.error())
<< "Both ssl_cert and ssl_key must be specified for gRPC TLS";
Throw<std::runtime_error>("Incomplete TLS configuration for gRPC");
}
JLOG(journal_.info()) << "gRPC TLS enabled with certificate: " << *sslCertPath_;
}
// Validate TLS configuration consistency: ssl_cert_chain only makes sense when TLS is
// enabled
if (sslCertChainPath_.has_value() &&
(!sslCertPath_.has_value() || !sslKeyPath_.has_value()))
{
JLOG(journal_.error())
<< "ssl_cert_chain specified for gRPC without both ssl_cert and ssl_key; "
<< "this is an invalid TLS configuration";
Throw<std::runtime_error>(
"Invalid gRPC TLS configuration: ssl_cert_chain requires both ssl_cert and "
"ssl_key");
}
// Validate TLS configuration consistency: ssl_client_ca only makes sense when TLS is
// enabled
if (sslClientCAPath_.has_value() && (!sslCertPath_.has_value() || !sslKeyPath_.has_value()))
{
JLOG(journal_.error())
<< "ssl_client_ca specified for gRPC without both ssl_cert and ssl_key; "
<< "this is an invalid TLS configuration";
Throw<std::runtime_error>(
"Invalid gRPC TLS configuration: ssl_client_ca requires both ssl_cert and ssl_key");
}
}
}
@@ -558,6 +602,104 @@ GRPCServerImpl::setupListeners()
return requests;
}
std::shared_ptr<grpc::ServerCredentials>
GRPCServerImpl::createServerCredentials()
{
if (not sslCertPath_.has_value() or not sslKeyPath_.has_value())
{
JLOG(journal_.info()) << "Configuring gRPC server without TLS";
return grpc::InsecureServerCredentials();
}
JLOG(journal_.info()) << "Configuring gRPC server with TLS";
try
{
boost::system::error_code ec;
grpc::SslServerCredentialsOptions sslOpts;
grpc::SslServerCredentialsOptions::PemKeyCertPair keyCertPair;
std::string const certContents = getFileContents(ec, *sslCertPath_);
if (ec)
{
JLOG(journal_.error()) << "Failed to read gRPC SSL certificate file: " << *sslCertPath_
<< " - " << ec.message(); // LCOV_EXCL_LINE
return nullptr;
}
std::string const keyContents = getFileContents(ec, *sslKeyPath_);
if (ec)
{
JLOG(journal_.error()) << "Failed to read gRPC SSL key file: " << *sslKeyPath_ << " - "
<< ec.message(); // LCOV_EXCL_LINE
return nullptr;
}
keyCertPair.private_key = keyContents;
// Read intermediate CA certificates for server certificate chain (optional)
std::string certChainContents;
if (sslCertChainPath_.has_value())
{
certChainContents = getFileContents(ec, *sslCertChainPath_);
if (ec)
{
JLOG(journal_.error())
<< "Failed to read gRPC SSL cert chain file: " << *sslCertChainPath_ << " - "
<< ec.message(); // LCOV_EXCL_LINE
return nullptr;
}
}
// Read CA certificate for client verification (mTLS, optional)
if (sslClientCAPath_.has_value())
{
auto const clientCAContents = getFileContents(ec, *sslClientCAPath_);
if (ec)
{
JLOG(journal_.error())
<< "Failed to read gRPC SSL client CA file: " << *sslClientCAPath_ << " - "
<< ec.message(); // LCOV_EXCL_LINE
return nullptr;
}
if (clientCAContents.empty())
{
JLOG(journal_.error())
<< "Empty/truncated gRPC SSL client CA file: " << *sslClientCAPath_
<< " - failed to configure mutual TLS"; // LCOV_EXCL_LINE
return nullptr;
}
sslOpts.pem_root_certs = clientCAContents;
sslOpts.client_certificate_request =
GRPC_SSL_REQUEST_AND_REQUIRE_CLIENT_CERTIFICATE_AND_VERIFY;
JLOG(journal_.info()) << "gRPC mutual TLS enabled - client certificates will be "
"required and verified";
}
// Combine server cert with intermediate CA certs for complete chain
keyCertPair.cert_chain = certContents;
if (!certChainContents.empty())
{
keyCertPair.cert_chain += '\n' + certChainContents;
JLOG(journal_.info()) << "gRPC server certificate chain configured with "
"intermediate CA certificates"; // LCOV_EXCL_LINE
}
sslOpts.pem_key_cert_pairs.push_back(keyCertPair);
JLOG(journal_.info()) << "gRPC TLS credentials configured successfully"; // LCOV_EXCL_LINE
return grpc::SslServerCredentials(sslOpts);
}
catch (std::exception const& e)
{
JLOG(journal_.error()) << "Exception while configuring gRPC TLS: "
<< e.what(); // LCOV_EXCL_LINE
return nullptr;
}
}
bool
GRPCServerImpl::start()
{
@@ -565,24 +707,63 @@ GRPCServerImpl::start()
if (serverAddress_.empty())
return false;
JLOG(journal_.info()) << "Starting gRPC server at " << serverAddress_;
// Determine TLS mode for logging
bool const tlsEnabled = sslCertPath_.has_value() && sslKeyPath_.has_value();
bool const mtlsEnabled = tlsEnabled && sslClientCAPath_.has_value();
std::string tlsMode = "without TLS";
if (mtlsEnabled)
{
tlsMode = "with mutual TLS (mTLS)";
}
else if (tlsEnabled)
{
tlsMode = "with TLS";
}
JLOG(journal_.info()) << "Starting gRPC server at " << serverAddress_ << " "
<< tlsMode; // LCOV_EXCL_LINE
grpc::ServerBuilder builder;
// Listen on the given address without any authentication mechanism.
// Actually binded port will be returned into "port" variable.
int port = 0;
builder.AddListeningPort(serverAddress_, grpc::InsecureServerCredentials(), &port);
// Create credentials (TLS or insecure) based on configuration
auto credentials = createServerCredentials();
if (!credentials)
{
JLOG(journal_.error()) << "Failed to create gRPC server credentials for " << serverAddress_
<< " (TLS mode: " << tlsMode
<< ") - server will not start"; // LCOV_EXCL_LINE
return false;
}
// Add listening port with appropriate credentials
builder.AddListeningPort(serverAddress_, credentials, &port);
// Register "service_" as the instance through which we'll communicate with
// clients. In this case it corresponds to an *asynchronous* service.
builder.RegisterService(&service_);
// Get hold of the completion queue used for the asynchronous communication
// with the gRPC runtime.
cq_ = builder.AddCompletionQueue();
// Finally assemble the server.
server_ = builder.BuildAndStart();
serverPort_ = static_cast<std::uint16_t>(port);
if (serverPort_ != 0u)
{
JLOG(journal_.info()) << "gRPC server started successfully on port " << serverPort_;
}
else
{
JLOG(journal_.error())
<< "Failed to start gRPC server at " << serverAddress_ << " (TLS mode: " << tlsMode
<< "); Possible causes: address already in use, invalid address format, or permission "
"denied"; // LCOV_EXCL_LINE
}
return static_cast<bool>(serverPort_);
}

View File

@@ -66,6 +66,13 @@ private:
std::vector<boost::asio::ip::address> secureGatewayIPs_;
// TLS certificate paths
std::optional<std::string> sslCertPath_;
std::optional<std::string> sslKeyPath_;
std::optional<std::string> sslCertChainPath_; // Intermediate CA certs for server cert chain
std::optional<std::string>
sslClientCAPath_; // CA cert for client certificate verification (mTLS)
beast::Journal journal_;
// typedef for function to bind a listener
@@ -124,6 +131,10 @@ public:
getEndpoint() const;
private:
// Create server credentials (TLS or insecure) based on configuration
std::shared_ptr<grpc::ServerCredentials>
createServerCredentials();
// Class encompassing the state and logic needed to serve a request.
template <class Request, class Response>
class CallData : public Processor,

View File

@@ -292,6 +292,17 @@ SHAMapStoreImp::run()
bool const readyToRotate = validatedSeq >= lastRotated + deleteInterval_ &&
canDelete_ >= lastRotated - 1 && healthWait() == keepGoing;
JLOG(journal_.debug()) << "run: Setting lastGoodValidatedLedger_ to " << validatedSeq;
{
// Note that this is set after the healthWait() check, so that we
// don't start the rotation until the validated ledger is fully
// processed. It is not guaranteed to be done at this point. It also
// allows the testLedgerGaps unit test to work.
std::unique_lock<std::mutex> const lock(mutex_);
lastGoodValidatedLedger_ = validatedSeq;
}
// will delete up to (not including) lastRotated
if (readyToRotate)
{
@@ -299,7 +310,8 @@ SHAMapStoreImp::run()
<< lastRotated << " deleteInterval " << deleteInterval_
<< " canDelete_ " << canDelete_ << " state "
<< app_.getOPs().strOperatingMode(false) << " age "
<< ledgerMaster_->getValidatedLedgerAge().count() << 's';
<< ledgerMaster_->getValidatedLedgerAge().count()
<< "s. Complete ledgers: " << ledgerMaster_->getCompleteLedgers();
clearPrior(lastRotated);
if (healthWait() == stopping)
@@ -359,7 +371,9 @@ SHAMapStoreImp::run()
clearCaches(validatedSeq);
});
JLOG(journal_.warn()) << "finished rotation " << validatedSeq;
JLOG(journal_.warn()) << "finished rotation. validatedSeq: " << validatedSeq
<< ", lastRotated: " << lastRotated
<< ". Complete ledgers: " << ledgerMaster_->getCompleteLedgers();
}
}
}
@@ -599,22 +613,41 @@ SHAMapStoreImp::clearPrior(LedgerIndex lastRotated)
SHAMapStoreImp::HealthResult
SHAMapStoreImp::healthWait()
{
auto index = ledgerMaster_->getValidLedgerIndex();
auto age = ledgerMaster_->getValidatedLedgerAge();
OperatingMode mode = netOPs_->getOperatingMode();
std::unique_lock lock(mutex_);
while (!stop_ && (mode != OperatingMode::FULL || age > ageThreshold_))
auto numMissing =
ledgerMaster_->missingFromCompleteLedgerRange(lastGoodValidatedLedger_, index);
while (!stop_ && (mode != OperatingMode::FULL || age > ageThreshold_ || numMissing > 0))
{
// this value shouldn't change, so grab it while we have the
// lock
auto const lowerBound = lastGoodValidatedLedger_;
lock.unlock();
JLOG(journal_.warn()) << "Waiting " << recoveryWaitTime_.count()
<< "s for node to stabilize. state: "
<< app_.getOPs().strOperatingMode(mode, false) << ". age "
<< age.count() << 's';
auto const stream =
mode != OperatingMode::FULL || age > ageThreshold_ ? journal_.warn() : journal_.info();
JLOG(stream) << "Waiting " << recoveryWaitTime_.count()
<< "s for node to stabilize. state: "
<< app_.getOPs().strOperatingMode(mode, false) << ". age " << age.count()
<< "s. Missing ledgers: " << numMissing << ". Expect: " << lowerBound << "-"
<< index << ". Complete ledgers: " << ledgerMaster_->getCompleteLedgers();
std::this_thread::sleep_for(recoveryWaitTime_);
index = ledgerMaster_->getValidLedgerIndex();
age = ledgerMaster_->getValidatedLedgerAge();
mode = netOPs_->getOperatingMode();
numMissing =
lowerBound == 0 ? 0 : ledgerMaster_->missingFromCompleteLedgerRange(lowerBound, index);
lock.lock();
}
JLOG(journal_.debug()) << "healthWait: Setting lastGoodValidatedLedger_ to " << index;
lastGoodValidatedLedger_ = index;
return stop_ ? stopping : keepGoing;
}

View File

@@ -70,6 +70,11 @@ private:
std::thread thread_;
bool stop_ = false;
bool healthy_ = true;
// Used to prevent ledger gaps from forming during online deletion. Keeps
// track of the last validated ledger that was processed without gaps. There
// are no guarantees about gaps while online delete is not running. For
// that, use advisory_delete and check for gaps externally.
LedgerIndex lastGoodValidatedLedger_ = 0;
mutable std::condition_variable cond_;
mutable std::condition_variable rendezvous_;
mutable std::mutex mutex_;
@@ -83,11 +88,11 @@ private:
std::uint32_t deleteBatch_ = 100;
std::chrono::milliseconds backOff_{100};
std::chrono::seconds ageThreshold_{60};
/// If the node is out of sync during an online_delete healthWait()
/// call, sleep the thread for this time, and continue checking until
/// recovery.
/// If the node is out of sync, or any recent ledgers are not
/// available during an online_delete healthWait() call, sleep
/// the thread for this time, and continue checking until recovery.
/// See also: "recovery_wait_seconds" in xrpld-example.cfg
std::chrono::seconds recoveryWaitTime_{5};
std::chrono::seconds recoveryWaitTime_{1};
// these do not exist upon SHAMapStore creation, but do exist
// as of run() or before
@@ -204,6 +209,8 @@ private:
enum HealthResult { stopping, keepGoing };
[[nodiscard]] HealthResult
healthWait();
bool
hasCompleteRange(LedgerIndex first, LedgerIndex last);
public:
void