diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index f2cddac488..2229332e41 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -1,5 +1,5 @@ { - "image_tag": "sha-473fe44", + "image_tag": "sha-060957e", "configs": { "ubuntu": [ { diff --git a/.github/workflows/build-nix-images.yml b/.github/workflows/build-nix-images.yml index e47a1f93ff..8ecf4912f5 100644 --- a/.github/workflows/build-nix-images.yml +++ b/.github/workflows/build-nix-images.yml @@ -5,15 +5,13 @@ on: branches: - develop paths: - - ".github/workflows/build-nix-images.yml" - "flake.nix" - "flake.lock" - "rust-toolchain.toml" - "nix/**" - "!nix/docker/README.md" - "!nix/devshell.nix" - - "!nix/check-tools/*.txt" - - "bin/check-tools.sh" + - "!nix/check-tools/**" - "bin/default-loader-path.sh" - "bin/install-sanitizer-libs.sh" pull_request: @@ -25,7 +23,7 @@ on: - "nix/**" - "!nix/docker/README.md" - "!nix/devshell.nix" - - "!nix/check-tools/*.txt" + - "!nix/check-tools/**" - "bin/check-tools.sh" - "bin/default-loader-path.sh" - "bin/install-sanitizer-libs.sh" diff --git a/.github/workflows/cargo-audit.yml b/.github/workflows/cargo-audit.yml index 6ddc6cdac9..6e613b822a 100644 --- a/.github/workflows/cargo-audit.yml +++ b/.github/workflows/cargo-audit.yml @@ -34,7 +34,7 @@ permissions: jobs: audit: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e permissions: contents: read # Needed to open an issue on scheduled failures. diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index d4fb6faeca..735ddfc906 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -41,7 +41,7 @@ env: jobs: build: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 68ab531882..8e3b80e318 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -34,7 +34,7 @@ jobs: needs: [determine-files] if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }} runs-on: ["self-hosted", "Linux", "X64", "heavy"] - container: "ghcr.io/xrplf/xrpld/nix-debian:sha-473fe44" + container: "ghcr.io/xrplf/xrpld/nix-debian:sha-060957e" permissions: contents: read issues: write diff --git a/.github/workflows/reusable-package.yml b/.github/workflows/reusable-package.yml index 58adc53dc8..9dbc027176 100644 --- a/.github/workflows/reusable-package.yml +++ b/.github/workflows/reusable-package.yml @@ -209,6 +209,23 @@ jobs: } echo "package=${package}" >>"${GITHUB_OUTPUT}" + # Debian 11 went end-of-life on 2026-08-31 + # (https://www.debian.org/News/2026/20260831) and its packages are + # already partly gone from deb.debian.org, so switch to the + # snapshot.debian.org entries the image ships commented out in its + # sources.list: they are pinned to the snapshot the image was built + # from, so they serve every version it needs and never go away. + # Snapshots keep their original, long-passed Valid-Until, hence the + # disabled check; the retries absorb snapshot.debian.org's throttling. + - name: Switch Debian 11 to snapshot.debian.org + if: ${{ matrix.image == 'debian:11' }} + run: | + sed -i 's|^deb |# deb |; s|^# deb http://snapshot|deb http://snapshot|' /etc/apt/sources.list + printf '%s\n' \ + 'Acquire::Check-Valid-Until "false";' \ + 'Acquire::Retries "3";' \ + >/etc/apt/apt.conf.d/99snapshot + - name: Install the DEB if: ${{ matrix.package_type == 'deb' }} env: diff --git a/.github/workflows/reusable-rust.yml b/.github/workflows/reusable-rust.yml index a0199f0129..12a31aacbd 100644 --- a/.github/workflows/reusable-rust.yml +++ b/.github/workflows/reusable-rust.yml @@ -1,8 +1,9 @@ # Clippy, coverage and documentation for the Rust crates in crates/. Each runs # as an independent job on a GitHub-hosted runner, but inside the same container # image used to build the crates in the C++/Corrosion path, so the toolchain -# (and therefore the lints, coverage instrumentation and the cargo cache) matches -# what production builds use. +# (and therefore the lints and the cargo cache) matches what production builds +# use. Coverage is the exception: it needs the nightly rustc that honours +# #[coverage(off)], which the image carries alongside the pinned stable. # # Rust unit tests are deliberately NOT run here. They run as part of the C++ # build (reusable-build-test-config.yml), which already compiles the crates on a @@ -27,7 +28,7 @@ permissions: jobs: clippy: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -40,11 +41,14 @@ jobs: coverage: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Use the nightly Rust toolchain + run: rust-nightly path >>"${GITHUB_PATH}" + - name: Use cargo artifacts cache uses: ./.github/actions/cargo-cache @@ -66,7 +70,7 @@ jobs: doc: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/reusable-upload-recipe.yml b/.github/workflows/reusable-upload-recipe.yml index 69de12db0e..0511458e7f 100644 --- a/.github/workflows/reusable-upload-recipe.yml +++ b/.github/workflows/reusable-upload-recipe.yml @@ -40,7 +40,7 @@ defaults: jobs: upload: runs-on: ubuntu-latest - container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-473fe44 + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-060957e env: REMOTE_NAME: ${{ inputs.remote_name }} CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f223ab1684..8721ad7863 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,6 +70,11 @@ repos: language: system types: [rust] pass_filenames: false # rustfmt formats the whole workspace + - id: check-coverage-attrs + name: check Rust coverage attributes + entry: ./bin/pre-commit/check_rust_coverage_attrs.py + language: python + files: ^crates/.*\.rs$ - repo: https://github.com/BlankSpruce/gersemi-pre-commit rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7 diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index ed27312023..192aa7272e 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -41,6 +41,7 @@ Version 3.4.0 is not yet released. These changes are available in the 3.4.0 beta - `gateway_balances`: The `account` and `ident` fields now return an `invalidParams` error if the value is not a string, instead of an `internal` error. [#7655](https://github.com/XRPLF/rippled/pull/7655) - `account_lines`: The `peer` field now returns an error if the value is not a string. [#7728](https://github.com/XRPLF/rippled/pull/7728) - `ledger`: `delivered_amount` is now included in the metadata of successful `AccountDelete` transactions when transactions are expanded (`expand`, or admin-only `full`). Previously it was only added for `Payment` and `CheckCash`, which made `ledger` inconsistent with `tx` and `account_tx`. [#5706](https://github.com/XRPLF/rippled/pull/5706) +- `noripple_check`: The `transactions` field is no longer included in error responses; it is still returned (possibly as an empty array) whenever `transactions` is `true` and the request succeeds. A malformed `account` is now rejected before the ledger is looked up, so that error response no longer carries the `ledger_hash`, `ledger_index`, and `validated` fields ([#6303](https://github.com/XRPLF/rippled/pull/6303)). ## XRP Ledger server version 3.3.0 diff --git a/bin/check-tools.sh b/bin/check-tools.sh index 8273375428..ed76861aa4 100755 --- a/bin/check-tools.sh +++ b/bin/check-tools.sh @@ -158,6 +158,7 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then check cargo-nextest cargo nextest --version check clippy-driver check rust-analyzer + check rust-nightly rust-nightly run rustc --version check rustc check rustfmt fi diff --git a/bin/pre-commit/check_rust_coverage_attrs.py b/bin/pre-commit/check_rust_coverage_attrs.py new file mode 100755 index 0000000000..dba56d5756 --- /dev/null +++ b/bin/pre-commit/check_rust_coverage_attrs.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Check that Rust unit tests stay out of the coverage report. + +cargo-llvm-cov instruments the test code along with everything else, so a test +module that is not excluded counts its own body as covered and inflates the +reported number. Excluding it takes two attributes: + + * every `#[cfg(test)]` module carries + `#[cfg_attr(coverage_nightly, coverage(off))]`; + * every crate root (lib.rs, main.rs) carries + `#![cfg_attr(coverage_nightly, feature(coverage_attribute))]`, which the + attribute above needs in order to compile. + +Both are inert outside the coverage job: cargo-llvm-cov defines +`coverage_nightly` only when it runs on a nightly toolchain. + +The crate-root gate is checked even in a crate that has no tests yet, because +that is what lets the first test module added later carry the attribute without +a build failure. Missing it is a hard error, so it cannot go unnoticed; a +missing `coverage(off)` fails open, which is why this check exists. + +Matching is on exact attribute text, which works because `cargo fmt` runs over +the whole workspace in the hook ahead of this one: rustfmt puts every attribute +on its own line and normalizes what is inside it, turning `#[cfg( test )]` +and `#[cfg(test,)]` alike into `#[cfg(test)]`. So there is nothing here that +parses Rust. The price is that a cfg this file does not spell out literally -- +`all(test, ...)`, `any(test, ...)`, `not(test)` -- is reported rather than +classified, on the grounds that guessing at coverage semantics is how a check +like this ends up quietly wrong. + +Usage: ./bin/pre-commit/check_rust_coverage_attrs.py ... + +Exit status is non-zero if any violation is found. +""" + +import re +import sys +from dataclasses import dataclass +from pathlib import Path + +CRATE_ROOTS = {"lib.rs", "main.rs"} + +FEATURE_ATTR = "#![cfg_attr(coverage_nightly, feature(coverage_attribute))]" +COVERAGE_OFF_ATTR = "#[cfg_attr(coverage_nightly, coverage(off))]" +CFG_TEST_ATTR = "#[cfg(test)]" + +# Any other cfg that mentions `test`. String literals are blanked before this +# runs, so `feature = "test"` does not read as the `test` cfg. +RE_CFG_MENTIONS_TEST = re.compile(r"^#\[cfg\(.*\btest\b.*\)\]$") +RE_STRING = re.compile(r'"(?:[^"\\]|\\.)*"') +RE_MOD = re.compile(r"^(?:pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)") + + +@dataclass(frozen=True) +class Finding: + line: int + label: str + message: str + + +def _check_module(attrs: list[str], line: int, name: str) -> list[Finding]: + """Findings for one module, given the attributes attached to it.""" + if COVERAGE_OFF_ATTR in attrs: + return [] # excluded from coverage; which cfg gates it does not matter + if CFG_TEST_ATTR in attrs: + return [ + Finding( + line, + "missing-coverage-off", + f"`mod {name}` is #[cfg(test)] but not excluded from coverage; " + f"add {COVERAGE_OFF_ATTR}", + ) + ] + unclassified = [ + attr for attr in attrs if RE_CFG_MENTIONS_TEST.match(RE_STRING.sub('""', attr)) + ] + if unclassified: + return [ + Finding( + line, + "unclassified-cfg", + f"`mod {name}` is gated on {unclassified[0]}, which this check " + f"cannot tell apart from a module that ships in the library; " + f"add {COVERAGE_OFF_ATTR} if it is test-only, or teach this " + f"check the cfg if it is not", + ) + ] + return [] + + +def _check_test_modules(lines: list[str]) -> list[Finding]: + """Findings for every test module that is not excluded from coverage.""" + findings: list[Finding] = [] + attrs: list[str] = [] + attrs_line = 0 + for number, raw in enumerate(lines, start=1): + stripped = raw.strip() + # Blank lines and comments are allowed between an attribute and its item. + if not stripped or stripped.startswith("//"): + continue + if stripped.startswith("#["): + if not attrs: + attrs_line = number + attrs.append(stripped) + continue + module = RE_MOD.match(stripped) + if module is not None and attrs: + findings += _check_module(attrs, attrs_line, module.group(1)) + attrs = [] + return findings + + +def _check_crate_root(name: str, lines: list[str]) -> list[Finding]: + """A finding if a crate root is missing the coverage_attribute feature gate.""" + if name not in CRATE_ROOTS: + return [] + if any(line.strip() == FEATURE_ATTR for line in lines): + return [] + return [ + Finding( + 1, + "missing-feature-gate", + f"crate root is missing {FEATURE_ATTR}", + ) + ] + + +def check_source(name: str, text: str) -> list[Finding]: + """Findings for one file's contents; `name` is its base name (lib.rs, ...).""" + lines = text.splitlines() + return _check_crate_root(name, lines) + _check_test_modules(lines) + + +def check_file(path: Path) -> list[Finding]: + return check_source(path.name, path.read_text(encoding="utf-8")) + + +def main() -> int: + total = 0 + for path in (Path(name) for name in sys.argv[1:]): + for finding in check_file(path): + total += 1 + print(f"{path}:{finding.line}: {finding.label}: {finding.message}") + return 1 if total else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/crates/Cargo.toml b/crates/Cargo.toml index 0bb0e9c550..36c79f1752 100644 --- a/crates/Cargo.toml +++ b/crates/Cargo.toml @@ -8,6 +8,9 @@ cxx = { version = "1.0.198", features = ["c++20"] } [workspace.package] edition = "2024" +[workspace.lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage)', 'cfg(coverage_nightly)' ] } + [profile.release] opt-level = 3 overflow-checks = true diff --git a/crates/hello_world/Cargo.toml b/crates/hello_world/Cargo.toml index 2e5a329c9a..50f02cc95f 100644 --- a/crates/hello_world/Cargo.toml +++ b/crates/hello_world/Cargo.toml @@ -8,3 +8,6 @@ crate-type = ["staticlib"] [dependencies] cxx.workspace = true + +[lints] +workspace = true diff --git a/crates/hello_world/src/lib.rs b/crates/hello_world/src/lib.rs index b1cb121fa0..6f69801a1d 100644 --- a/crates/hello_world/src/lib.rs +++ b/crates/hello_world/src/lib.rs @@ -1,3 +1,5 @@ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] + #[cxx::bridge(namespace = "rs::hello_world")] mod ffi { extern "Rust" { @@ -8,3 +10,14 @@ mod ffi { pub fn hello_world() -> String { "hello_world".to_string() } + +#[cfg(test)] +#[cfg_attr(coverage_nightly, coverage(off))] +mod tests { + use super::*; + + #[test] + fn hello_world_returns_hello_world() { + assert_eq!(hello_world(), "hello_world") + } +} diff --git a/include/xrpl/peerfinder/Config.h b/include/xrpl/peerfinder/Config.h index 3326ae8a97..fa2d2d78e8 100644 --- a/include/xrpl/peerfinder/Config.h +++ b/include/xrpl/peerfinder/Config.h @@ -26,7 +26,8 @@ struct Config /** * The largest number of public peer slots to allow. * This includes both inbound and outbound, but does not include - * fixed peers. + * fixed peers. A configuration built by `makeConfig` always holds + * `maxPeers == inPeers + outPeers`. */ std::size_t maxPeers{tuning::kDefaultMaxPeers}; diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index ecf7970aba..3706ef94e3 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -6,6 +6,7 @@ #include #include #include // IWYU pragma: keep +#include #include #include #include @@ -301,6 +302,63 @@ verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 co NotTEC checkEncryptedAmountFormat(STObject const& object); +/** + * @brief Checks whether a holder's issuer mirror is encrypted under the + * issuance's currently registered issuer key. + * + * Verifies that the holder's issuer mirror epoch matches the active issuer key + * epoch on the issuance. An absent mirror epoch defaults to epoch 0. A holder without an issuer + * mirror is considered stale, as there is no key anchor for future re-encryptions. + * + * @param issuance The MPTokenIssuance ledger object. + * @param mptoken The holder's MPToken ledger object. + * @return true if the MPToken's issuer mirror is current. false if stale. + */ +[[nodiscard]] bool +isIssuerMirrorCurrent(SLE const& issuance, SLE const& mptoken); + +/** + * @brief Checks whether a holder's auditor mirror is encrypted under the + * issuance's currently registered auditor key. + * + * Verifies that the holder's auditor mirror epoch matches the active auditor key + * epoch on the issuance. An absent mirror epoch defaults to epoch 0. An issuance + * without an auditor key requires no auditor mirror and is considered current. + * + * @param issuance The MPTokenIssuance ledger object. + * @param mptoken The holder's MPToken ledger object. + * @return true if the auditor mirror is current or not required. + */ +[[nodiscard]] bool +isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken); + +/** + * @brief Checks whether each mirror a holder is required to have is encrypted + * under the issuance's currently registered ElGamal keys. + * + * Verifies that both the issuer mirror and the auditor mirror (if required) + * are current. This serves as a combined check, ensuring all necessary + * holder mirror epochs match the active key epochs on the issuance. + * + * @param issuance The MPTokenIssuance ledger object. + * @param mptoken The holder's MPToken ledger object. + * @return true if the required mirrors are current. + */ +[[nodiscard]] bool +areMirrorsCurrent(SLE const& issuance, SLE const& mptoken); + +/** + * @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs. + * + * Call this after writing mirror ciphertexts under the issuance's currently + * registered keys, so that the mirrors read as current afterwards. + * + * @param issuance The MPTokenIssuance ledger object. + * @param mptoken The holder's MPToken ledger entry to update. + */ +void +setMirrorEpochs(SLE const& issuance, SLE& mptoken); + /** * @brief Verifies revealed amount encryptions for all recipients. * diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 730d021254..5702b01d1d 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -129,8 +129,11 @@ enum TEMcodes : TERUnderlyingType { temARRAY_TOO_LARGE, temBAD_TRANSFER_FEE, temINVALID_INNER_BATCH, + temBAD_MPT, temBAD_CIPHERTEXT, + temINVALID_BYTECODE, + temTEMP_DISABLED, }; //------------------------------------------------------------------------------ @@ -179,6 +182,8 @@ enum TEFcodes : TERUnderlyingType { tefINVALID_LEDGER_FIX_TYPE, tefNO_DST_PARTIAL, tefBAD_PATH_COUNT, + tefNO_BYTECODE, + tefBYTECODE_NOT_INCLUDED, }; //------------------------------------------------------------------------------ @@ -370,6 +375,8 @@ enum TECcodes : TERUnderlyingType { tecNO_DELEGATE_PERMISSION = 198, tecBAD_PROOF = 199, tecNO_SPONSOR_PERMISSION = 200, + tecOUT_OF_GAS = 201, + tecBYTECODE_REJECTED = 202, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 84452eb18e..e63a7f515d 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -15,6 +15,8 @@ // Add new amendments to the top of this list. // Keep it sorted in reverse chronological order. +XRPL_FEATURE(SmartEscrow, Supported::No, VoteBehavior::DefaultNo) +XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index be12ad6348..04b390a7a4 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -309,6 +309,11 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({ {sfBaseFeeDrops, SoeOptional}, {sfReserveBaseDrops, SoeOptional}, {sfReserveIncrementDrops, SoeOptional}, + // Smart Escrow fields + {sfGasLimit, SoeOptional}, + {sfBytecodeSizeLimit, SoeOptional}, + {sfGasPrice, SoeOptional}, + {sfPreviousTxnID, SoeOptional}, {sfPreviousTxnLgrSeq, SoeOptional}, })) @@ -339,6 +344,8 @@ LEDGER_ENTRY(ltESCROW, 0x0075, Escrow, escrow, ({ {sfCondition, SoeOptional}, {sfCancelAfter, SoeOptional}, {sfFinishAfter, SoeOptional}, + {sfBytecode, SoeOptional}, + {sfData, SoeOptional}, {sfSourceTag, SoeOptional}, {sfDestinationTag, SoeOptional}, {sfOwnerNode, SoeRequired}, @@ -429,6 +436,8 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ {sfConfidentialBalanceVersion, SoeDefault}, {sfIssuerEncryptedBalance, SoeOptional}, {sfAuditorEncryptedBalance, SoeOptional}, + {sfIssuerKeyMirrorEpoch, SoeOptional}, + {sfAuditorKeyMirrorEpoch, SoeOptional}, {sfHolderEncryptionKey, SoeOptional}, })) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 0a88a6a191..2cf35743ae 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -123,6 +123,11 @@ TYPED_SFIELD(sfIssuerKeyEpoch, UINT32, 77) TYPED_SFIELD(sfAuditorKeyEpoch, UINT32, 78) TYPED_SFIELD(sfIssuerKeyMirrorEpoch, UINT32, 79) TYPED_SFIELD(sfAuditorKeyMirrorEpoch, UINT32, 80) +TYPED_SFIELD(sfGasLimit, UINT32, 81) +TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82) +TYPED_SFIELD(sfGasPrice, UINT32, 83) +TYPED_SFIELD(sfGas, UINT32, 84) +TYPED_SFIELD(sfGasUsed, UINT32, 85) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -238,6 +243,7 @@ TYPED_SFIELD(sfManagementFeeOutstanding, NUMBER, 17, SField::kSmdNeedsAsset // 32-bit signed (common) TYPED_SFIELD(sfLoanScale, INT32, 1) TYPED_SFIELD(sfRemainingOwnerCountDelta, INT32, 2) +TYPED_SFIELD(sfVMReturnCode, INT32, 3) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) @@ -322,6 +328,7 @@ TYPED_SFIELD(sfAuditorEncryptedAmount, VL, 43) TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44) TYPED_SFIELD(sfAmountCommitment, VL, 45) TYPED_SFIELD(sfBalanceCommitment, VL, 46) +TYPED_SFIELD(sfBytecode, VL, 47) // account (common) TYPED_SFIELD(sfAccount, ACCOUNT, 1) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dbf9b66ac7..454aa85ffd 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -66,11 +66,13 @@ TRANSACTION(ttPAYMENT, 0, Payment, #endif TRANSACTION(ttESCROW_CREATE, 1, EscrowCreate, ({.delegable = Delegation::Delegable}), ({ {sfDestination, SoeRequired}, + {sfDestinationTag, SoeOptional}, {sfAmount, SoeRequired, SoeMptSupported}, {sfCondition, SoeOptional}, {sfCancelAfter, SoeOptional}, {sfFinishAfter, SoeOptional}, - {sfDestinationTag, SoeOptional}, + {sfBytecode, SoeOptional}, + {sfData, SoeOptional}, })) /** This transaction type completes an existing escrow. */ @@ -83,6 +85,7 @@ TRANSACTION(ttESCROW_FINISH, 2, EscrowFinish, ({.delegable = Delegation::Delegab {sfFulfillment, SoeOptional}, {sfCondition, SoeOptional}, {sfCredentialIDs, SoeOptional}, + {sfGas, SoeOptional}, })) @@ -1161,6 +1164,10 @@ TRANSACTION(ttFEE, 101, SetFee, {sfBaseFeeDrops, SoeOptional}, {sfReserveBaseDrops, SoeOptional}, {sfReserveIncrementDrops, SoeOptional}, + // Smart Escrow fields + {sfGasLimit, SoeOptional}, + {sfBytecodeSizeLimit, SoeOptional}, + {sfGasPrice, SoeOptional}, })) /** This system-generated transaction type is used to update the network's negative UNL diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 63e877ca31..b294a846da 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -278,6 +278,7 @@ JSS(frozen_balances); // out: GatewayBalances JSS(full); // in: LedgerClearer, handlers/Ledger JSS(full_reply); // out: PathFind JSS(fullbelow_size); // out: GetCounts +JSS(gateway); // in: noripple_check JSS(git); // out: server_info JSS(good); // out: RPCVersion JSS(hash); // out: NetworkOPs, InboundLedger, LedgerToJson, STTx; field @@ -481,6 +482,7 @@ JSS(ports); // out: NetworkOPs JSS(previous); // out: Reservations JSS(previous_ledger); // out: LedgerPropose JSS(price); // out: amm_info, AuctionSlot +JSS(problems); // out: noripple_check JSS(proof); // in: BookOffers JSS(propose_seq); // out: LedgerPropose JSS(proposers); // out: NetworkOPs, LedgerConsensus @@ -660,6 +662,7 @@ JSS(url); // in/out: Subscribe, Unsubscribe JSS(url_password); // in: Subscribe JSS(url_username); // in: Subscribe JSS(urlgravatar); // +JSS(user); // in: noripple_check JSS(username); // in: Subscribe JSS(validated); // out: NetworkOPs, RPCHelpers, AccountTx*, Tx JSS(validator_list_expires); // out: NetworkOps, ValidatorList diff --git a/include/xrpl/protocol_autogen/ledger_entries/Escrow.h b/include/xrpl/protocol_autogen/ledger_entries/Escrow.h index 106d69722f..fc01c8b858 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/Escrow.h +++ b/include/xrpl/protocol_autogen/ledger_entries/Escrow.h @@ -174,6 +174,54 @@ public: return this->sle_->isFieldPresent(sfFinishAfter); } + /** + * @brief Get sfBytecode (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getBytecode() const + { + if (hasBytecode()) + return this->sle_->at(sfBytecode); + return std::nullopt; + } + + /** + * @brief Check if sfBytecode is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasBytecode() const + { + return this->sle_->isFieldPresent(sfBytecode); + } + + /** + * @brief Get sfData (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getData() const + { + if (hasData()) + return this->sle_->at(sfData); + return std::nullopt; + } + + /** + * @brief Check if sfData is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasData() const + { + return this->sle_->isFieldPresent(sfData); + } + /** * @brief Get sfSourceTag (SoeOptional) * @return The field value, or std::nullopt if not present. @@ -453,6 +501,28 @@ public: return *this; } + /** + * @brief Set sfBytecode (SoeOptional) + * @return Reference to this builder for method chaining. + */ + EscrowBuilder& + setBytecode(std::decay_t const& value) + { + object_[sfBytecode] = value; + return *this; + } + + /** + * @brief Set sfData (SoeOptional) + * @return Reference to this builder for method chaining. + */ + EscrowBuilder& + setData(std::decay_t const& value) + { + object_[sfData] = value; + return *this; + } + /** * @brief Set sfSourceTag (SoeOptional) * @return Reference to this builder for method chaining. diff --git a/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h b/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h index 21478f749d..826ad6983e 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h +++ b/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h @@ -213,6 +213,78 @@ public: return this->sle_->isFieldPresent(sfReserveIncrementDrops); } + /** + * @brief Get sfGasLimit (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getGasLimit() const + { + if (hasGasLimit()) + return this->sle_->at(sfGasLimit); + return std::nullopt; + } + + /** + * @brief Check if sfGasLimit is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasGasLimit() const + { + return this->sle_->isFieldPresent(sfGasLimit); + } + + /** + * @brief Get sfBytecodeSizeLimit (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getBytecodeSizeLimit() const + { + if (hasBytecodeSizeLimit()) + return this->sle_->at(sfBytecodeSizeLimit); + return std::nullopt; + } + + /** + * @brief Check if sfBytecodeSizeLimit is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasBytecodeSizeLimit() const + { + return this->sle_->isFieldPresent(sfBytecodeSizeLimit); + } + + /** + * @brief Get sfGasPrice (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getGasPrice() const + { + if (hasGasPrice()) + return this->sle_->at(sfGasPrice); + return std::nullopt; + } + + /** + * @brief Check if sfGasPrice is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasGasPrice() const + { + return this->sle_->isFieldPresent(sfGasPrice); + } + /** * @brief Get sfPreviousTxnID (SoeOptional) * @return The field value, or std::nullopt if not present. @@ -375,6 +447,39 @@ public: return *this; } + /** + * @brief Set sfGasLimit (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FeeSettingsBuilder& + setGasLimit(std::decay_t const& value) + { + object_[sfGasLimit] = value; + return *this; + } + + /** + * @brief Set sfBytecodeSizeLimit (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FeeSettingsBuilder& + setBytecodeSizeLimit(std::decay_t const& value) + { + object_[sfBytecodeSizeLimit] = value; + return *this; + } + + /** + * @brief Set sfGasPrice (SoeOptional) + * @return Reference to this builder for method chaining. + */ + FeeSettingsBuilder& + setGasPrice(std::decay_t const& value) + { + object_[sfGasPrice] = value; + return *this; + } + /** * @brief Set sfPreviousTxnID (SoeOptional) * @return Reference to this builder for method chaining. diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h index 874d779d09..c1601fc2fa 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h @@ -268,6 +268,54 @@ public: return this->sle_->isFieldPresent(sfAuditorEncryptedBalance); } + /** + * @brief Get sfIssuerKeyMirrorEpoch (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getIssuerKeyMirrorEpoch() const + { + if (hasIssuerKeyMirrorEpoch()) + return this->sle_->at(sfIssuerKeyMirrorEpoch); + return std::nullopt; + } + + /** + * @brief Check if sfIssuerKeyMirrorEpoch is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasIssuerKeyMirrorEpoch() const + { + return this->sle_->isFieldPresent(sfIssuerKeyMirrorEpoch); + } + + /** + * @brief Get sfAuditorKeyMirrorEpoch (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorKeyMirrorEpoch() const + { + if (hasAuditorKeyMirrorEpoch()) + return this->sle_->at(sfAuditorKeyMirrorEpoch); + return std::nullopt; + } + + /** + * @brief Check if sfAuditorKeyMirrorEpoch is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorKeyMirrorEpoch() const + { + return this->sle_->isFieldPresent(sfAuditorKeyMirrorEpoch); + } + /** * @brief Get sfHolderEncryptionKey (SoeOptional) * @return The field value, or std::nullopt if not present. @@ -471,6 +519,28 @@ public: return *this; } + /** + * @brief Set sfIssuerKeyMirrorEpoch (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setIssuerKeyMirrorEpoch(std::decay_t const& value) + { + object_[sfIssuerKeyMirrorEpoch] = value; + return *this; + } + + /** + * @brief Set sfAuditorKeyMirrorEpoch (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setAuditorKeyMirrorEpoch(std::decay_t const& value) + { + object_[sfAuditorKeyMirrorEpoch] = value; + return *this; + } + /** * @brief Set sfHolderEncryptionKey (SoeOptional) * @return Reference to this builder for method chaining. diff --git a/include/xrpl/protocol_autogen/transactions/EscrowCreate.h b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h index 3d28a12cee..78f9f00033 100644 --- a/include/xrpl/protocol_autogen/transactions/EscrowCreate.h +++ b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h @@ -58,6 +58,32 @@ public: return this->tx_->at(sfDestination); } + /** + * @brief Get sfDestinationTag (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getDestinationTag() const + { + if (hasDestinationTag()) + { + return this->tx_->at(sfDestinationTag); + } + return std::nullopt; + } + + /** + * @brief Check if sfDestinationTag is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasDestinationTag() const + { + return this->tx_->isFieldPresent(sfDestinationTag); + } + /** * @brief Get sfAmount (SoeRequired) * @note This field supports MPT (Multi-Purpose Token) amounts. @@ -149,29 +175,55 @@ public: } /** - * @brief Get sfDestinationTag (SoeOptional) + * @brief Get sfBytecode (SoeOptional) * @return The field value, or std::nullopt if not present. */ [[nodiscard]] - protocol_autogen::Optional - getDestinationTag() const + protocol_autogen::Optional + getBytecode() const { - if (hasDestinationTag()) + if (hasBytecode()) { - return this->tx_->at(sfDestinationTag); + return this->tx_->at(sfBytecode); } return std::nullopt; } /** - * @brief Check if sfDestinationTag is present. + * @brief Check if sfBytecode is present. * @return True if the field is present, false otherwise. */ [[nodiscard]] bool - hasDestinationTag() const + hasBytecode() const { - return this->tx_->isFieldPresent(sfDestinationTag); + return this->tx_->isFieldPresent(sfBytecode); + } + + /** + * @brief Get sfData (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getData() const + { + if (hasData()) + { + return this->tx_->at(sfData); + } + return std::nullopt; + } + + /** + * @brief Check if sfData is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasData() const + { + return this->tx_->isFieldPresent(sfData); } }; @@ -232,6 +284,17 @@ public: return *this; } + /** + * @brief Set sfDestinationTag (SoeOptional) + * @return Reference to this builder for method chaining. + */ + EscrowCreateBuilder& + setDestinationTag(std::decay_t const& value) + { + object_[sfDestinationTag] = value; + return *this; + } + /** * @brief Set sfAmount (SoeRequired) * @note This field supports MPT (Multi-Purpose Token) amounts. @@ -278,13 +341,24 @@ public: } /** - * @brief Set sfDestinationTag (SoeOptional) + * @brief Set sfBytecode (SoeOptional) * @return Reference to this builder for method chaining. */ EscrowCreateBuilder& - setDestinationTag(std::decay_t const& value) + setBytecode(std::decay_t const& value) { - object_[sfDestinationTag] = value; + object_[sfBytecode] = value; + return *this; + } + + /** + * @brief Set sfData (SoeOptional) + * @return Reference to this builder for method chaining. + */ + EscrowCreateBuilder& + setData(std::decay_t const& value) + { + object_[sfData] = value; return *this; } diff --git a/include/xrpl/protocol_autogen/transactions/EscrowFinish.h b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h index 1cbc60c738..9c24c7671b 100644 --- a/include/xrpl/protocol_autogen/transactions/EscrowFinish.h +++ b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h @@ -146,6 +146,32 @@ public: { return this->tx_->isFieldPresent(sfCredentialIDs); } + + /** + * @brief Get sfGas (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getGas() const + { + if (hasGas()) + { + return this->tx_->at(sfGas); + } + return std::nullopt; + } + + /** + * @brief Check if sfGas is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasGas() const + { + return this->tx_->isFieldPresent(sfGas); + } }; /** @@ -249,6 +275,17 @@ public: return *this; } + /** + * @brief Set sfGas (SoeOptional) + * @return Reference to this builder for method chaining. + */ + EscrowFinishBuilder& + setGas(std::decay_t const& value) + { + object_[sfGas] = value; + return *this; + } + /** * @brief Build and return the EscrowFinish wrapper. * @param publicKey The public key for signing. diff --git a/include/xrpl/protocol_autogen/transactions/SetFee.h b/include/xrpl/protocol_autogen/transactions/SetFee.h index 9513723e94..edcea7b734 100644 --- a/include/xrpl/protocol_autogen/transactions/SetFee.h +++ b/include/xrpl/protocol_autogen/transactions/SetFee.h @@ -254,6 +254,84 @@ public: { return this->tx_->isFieldPresent(sfReserveIncrementDrops); } + + /** + * @brief Get sfGasLimit (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getGasLimit() const + { + if (hasGasLimit()) + { + return this->tx_->at(sfGasLimit); + } + return std::nullopt; + } + + /** + * @brief Check if sfGasLimit is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasGasLimit() const + { + return this->tx_->isFieldPresent(sfGasLimit); + } + + /** + * @brief Get sfBytecodeSizeLimit (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getBytecodeSizeLimit() const + { + if (hasBytecodeSizeLimit()) + { + return this->tx_->at(sfBytecodeSizeLimit); + } + return std::nullopt; + } + + /** + * @brief Check if sfBytecodeSizeLimit is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasBytecodeSizeLimit() const + { + return this->tx_->isFieldPresent(sfBytecodeSizeLimit); + } + + /** + * @brief Get sfGasPrice (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getGasPrice() const + { + if (hasGasPrice()) + { + return this->tx_->at(sfGasPrice); + } + return std::nullopt; + } + + /** + * @brief Check if sfGasPrice is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasGasPrice() const + { + return this->tx_->isFieldPresent(sfGasPrice); + } }; /** @@ -386,6 +464,39 @@ public: return *this; } + /** + * @brief Set sfGasLimit (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SetFeeBuilder& + setGasLimit(std::decay_t const& value) + { + object_[sfGasLimit] = value; + return *this; + } + + /** + * @brief Set sfBytecodeSizeLimit (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SetFeeBuilder& + setBytecodeSizeLimit(std::decay_t const& value) + { + object_[sfBytecodeSizeLimit] = value; + return *this; + } + + /** + * @brief Set sfGasPrice (SoeOptional) + * @return Reference to this builder for method chaining. + */ + SetFeeBuilder& + setGasPrice(std::decay_t const& value) + { + object_[sfGasPrice] = value; + return *this; + } + /** * @brief Build and return the SetFee wrapper. * @param publicKey The public key for signing. diff --git a/nix/check-tools/macos.txt b/nix/check-tools/macos.txt index 8edfeef311..d2dee651f1 100644 --- a/nix/check-tools/macos.txt +++ b/nix/check-tools/macos.txt @@ -131,6 +131,9 @@ Rust toolchain: ✅ rust-analyzer rust-analyzer 1.97.1 (8bab26f4 2026-07-14) /nix/store/j6apc5pmd0giy15da9p650r8zklslmvi-rust-analyzer-preview-1.97.1-aarch64-apple-darwin/bin/rust-analyzer + ✅ rust-nightly + rustc 1.99.0-nightly (87e5904f5 2026-07-20) + /nix/store/fqpjz4l0nsnji8b2pz57mnj0akbp6hcl-rust-nightly/bin/rust-nightly ✅ rustc rustc 1.97.1 (8bab26f4f 2026-07-14) /nix/store/bnfk1sl4s9angb0vj1cj9a5y5zvqinwy-rust-minimal-1.97.1/bin/rustc @@ -140,4 +143,4 @@ Rust toolchain: Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set). -✅ All 44 checked tools are present and runnable. +✅ All 45 checked tools are present and runnable. diff --git a/nix/check-tools/nix-ubuntu-amd64.txt b/nix/check-tools/nix-ubuntu-amd64.txt index 28b6c38014..ba5d5e65b1 100644 --- a/nix/check-tools/nix-ubuntu-amd64.txt +++ b/nix/check-tools/nix-ubuntu-amd64.txt @@ -131,6 +131,9 @@ Rust toolchain: ✅ rust-analyzer rust-analyzer 1.97.1 (8bab26f 2026-07-14) /nix/store/lr3m97p3hx1k22a7c44pb0wa7rbayhfi-rust-analyzer-preview-1.97.1-x86_64-unknown-linux-gnu/bin/rust-analyzer + ✅ rust-nightly + rustc 1.99.0-nightly (87e5904f5 2026-07-20) + /nix/store/j7kf7a5h4xypzp6x1skg4dsdx2k4fwb3-rust-nightly/bin/rust-nightly ✅ rustc rustc 1.97.1 (8bab26f4f 2026-07-14) /nix/store/40d3mzka7r1ps71l0yv2fs6616nbw85m-rust-minimal-1.97.1/bin/rustc @@ -168,4 +171,4 @@ Mold: Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set). -✅ All 52 checked tools are present and runnable. +✅ All 53 checked tools are present and runnable. diff --git a/nix/check-tools/nix-ubuntu-arm64.txt b/nix/check-tools/nix-ubuntu-arm64.txt index b3b5885a7f..2b45230327 100644 --- a/nix/check-tools/nix-ubuntu-arm64.txt +++ b/nix/check-tools/nix-ubuntu-arm64.txt @@ -131,6 +131,9 @@ Rust toolchain: ✅ rust-analyzer rust-analyzer 1.97.1 (8bab26f 2026-07-14) /nix/store/262830dlw2517lnagfx7i7agqgl4fmsd-rust-analyzer-preview-1.97.1-aarch64-unknown-linux-gnu/bin/rust-analyzer + ✅ rust-nightly + rustc 1.99.0-nightly (87e5904f5 2026-07-20) + /nix/store/c59pxk1yikdlf129qwyg4fplmxcrha0k-rust-nightly/bin/rust-nightly ✅ rustc rustc 1.97.1 (8bab26f4f 2026-07-14) /nix/store/a6p27cg6b8szfixfyvkssx6l0c345zw8-rust-minimal-1.97.1/bin/rustc @@ -168,4 +171,4 @@ Mold: Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set). -✅ All 52 checked tools are present and runnable. +✅ All 53 checked tools are present and runnable. diff --git a/nix/docker/Dockerfile b/nix/docker/Dockerfile index 5506bc3c77..7eae693ad2 100644 --- a/nix/docker/Dockerfile +++ b/nix/docker/Dockerfile @@ -10,6 +10,8 @@ RUN mkdir -p ~/.config/nix && \ COPY nix/ci-env.nix /tmp/build/nix/ci-env.nix COPY nix/linux.nix /tmp/build/nix/linux.nix COPY nix/packages.nix /tmp/build/nix/packages.nix +COPY nix/rust-nightly.sh /tmp/build/nix/rust-nightly.sh +COPY nix/rust.nix /tmp/build/nix/rust.nix COPY nix/utils.nix /tmp/build/nix/utils.nix COPY flake.nix /tmp/build/ COPY flake.lock /tmp/build/ diff --git a/nix/packages.nix b/nix/packages.nix index c7972c9843..9af230097d 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -16,23 +16,7 @@ let exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@" ''; - # rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so - # cargo has a linker). That default may be different from the clang we pin here, - # so it shadows our clang and the build can silently use a different compiler - # version. Drop that cc from every propagation channel instead of pinning a - # replacement: the toolchain then carries no compiler and cargo just uses the - # active shell's stdenv cc. Must cover all channels — rust-overlay uses both - # propagatedBuildInputs and depsHostHostPropagated. - rustToolchainBase = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml; - rustToolchain = - let - defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv - withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath); - in - rustToolchainBase.overrideAttrs (old: { - propagatedBuildInputs = withoutDefaultCc (old.propagatedBuildInputs or [ ]); - depsHostHostPropagated = withoutDefaultCc (old.depsHostHostPropagated or [ ]); - }); + rust = import ./rust.nix { inherit pkgs; }; # Nix wraps its toolchain so that binaries are exposed only under unsuffixed # names (gcc, g++, clang-tidy, ...). Several tools probe for a @@ -108,41 +92,38 @@ in mkGcov ; - commonPackages = with pkgs; [ - clangToolLinks - runClangTidyLink - ccache - clangbuildanalyzer - clangTools - cmake - conan - curlMinimal # needed for codecov/codecov-action - doxygen - file # needed for cpack in Clio - gcovr - gh - git - git-cliff - git-lfs - gnumake - gnupg # needed for signing commits & codecov/codecov-action - graphviz - less # needed for git diff - mold - nettools # provides netstat, used to debug failures in CI - ninja - patchelf - perl # needed for openssl - pkg-config - pre-commit - python3 - runClangTidy - vim - zip - # Rust packages - cargo-audit - cargo-llvm-cov - cargo-nextest - rustToolchain - ]; + commonPackages = + (with pkgs; [ + clangToolLinks + runClangTidyLink + ccache + clangbuildanalyzer + clangTools + cmake + conan + curlMinimal # needed for codecov/codecov-action + doxygen + file # needed for cpack in Clio + gcovr + gh + git + git-cliff + git-lfs + gnumake + gnupg # needed for signing commits & codecov/codecov-action + graphviz + less # needed for git diff + mold + nettools # provides netstat, used to debug failures in CI + ninja + patchelf + perl # needed for openssl + pkg-config + pre-commit + python3 + runClangTidy + vim + zip + ]) + ++ rust.packages; } diff --git a/nix/rust-nightly.sh b/nix/rust-nightly.sh new file mode 100644 index 0000000000..263e647d8e --- /dev/null +++ b/nix/rust-nightly.sh @@ -0,0 +1,23 @@ +#!@runtimeShell@ +# Reaches the nightly Rust toolchain, which is deliberately kept off PATH. +# Packaged by nix/rust.nix, which explains why. + +set -euo pipefail + +usage() { + echo "usage: rust-nightly (path | run ...)" >&2 + exit 2 +} + +case "${1-}" in + path) printf '%s\n' "@rustNightlyBin@" ;; + run) + shift + if [[ $# -eq 0 ]]; then + usage + fi + export PATH="@rustNightlyBin@:${PATH}" + exec "$@" + ;; + *) usage ;; +esac diff --git a/nix/rust.nix b/nix/rust.nix new file mode 100644 index 0000000000..8be48dcad0 --- /dev/null +++ b/nix/rust.nix @@ -0,0 +1,84 @@ +# The Rust half of the tool set shared by the CI environment and the dev shell: +# the stable toolchain pinned by rust-toolchain.toml, the nightly the Rust +# coverage job needs, and the cargo plugins. Consumed by packages.nix. +{ pkgs }: +let + # rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so + # cargo has a linker). That default may be different from the clang we pin + # elsewhere, so it shadows our clang and the build can silently use a different + # compiler version. Drop that cc from every propagation channel instead of + # pinning a replacement: the toolchain then carries no compiler and cargo just + # uses the active shell's stdenv cc. + # + # The channel list is every list mkDerivation propagates to a dependent's + # environment (including the two legacy aliases). rust-overlay currently only + # uses propagatedBuildInputs and depsHostHostPropagated, but covering all of + # them means an upstream switch to another channel cannot quietly put the + # compiler back on PATH. + dropDefaultCc = + toolchain: + let + defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv + withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath); + in + toolchain.overrideAttrs ( + old: + pkgs.lib.genAttrs [ + "depsBuildBuildPropagated" + "propagatedNativeBuildInputs" # alias of depsBuildHostPropagated + "depsBuildTargetPropagated" + "depsHostHostPropagated" + "propagatedBuildInputs" # alias of depsHostTargetPropagated + "depsTargetTargetPropagated" + ] (channel: withoutDefaultCc (old.${channel} or [ ])) + ); + + rustToolchain = dropDefaultCc (pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml); + + # cargo-llvm-cov honours the #[coverage(off)] that keeps unit tests out of the + # coverage report only under a nightly rustc, and looks for llvm-profdata and + # llvm-cov in that same toolchain's sysroot — hence llvm-tools-preview. + # + # Not every nightly ships every component, so `nightly.latest` breaks on the + # days llvm-tools-preview is absent; selectLatestNightlyWith walks back to the + # newest one that has it. The result is the newest such nightly *known to the + # locked rust-overlay*, which means updating flake.lock moves the compiler that + # produces the coverage numbers — and with it the rustc version recorded in + # nix/check-tools/*.txt, so those snapshots need regenerating alongside. + rustNightly = dropDefaultCc ( + pkgs.rust-bin.selectLatestNightlyWith ( + toolchain: toolchain.minimal.override { extensions = [ "llvm-tools-preview" ]; } + ) + ); + + # A second toolchain cannot go on PATH: its cargo and rustc would collide with + # the pinned stable's in the ci-env buildEnv, which resolves collisions by + # picking one silently. Reaching the nightly only through this wrapper keeps it + # in the image closure (the Docker build copies the whole closure, not just + # what is linked into /bin) while leaving it inactive everywhere that does not + # ask for it. + # + # The script's `path` subcommand exists for scopes wider than one command — a + # CI job appending to $GITHUB_PATH, so that the cargo cache action's own + # `rustc -vV` probe, which runs in a step of its own, agrees with the toolchain + # the build will use. + rustNightlyScript = pkgs.replaceVarsWith { + name = "rust-nightly"; + src = ./rust-nightly.sh; + dir = "bin"; + isExecutable = true; + replacements = { + inherit (pkgs) runtimeShell; + rustNightlyBin = "${rustNightly}/bin"; + }; + }; +in +{ + packages = [ + pkgs.cargo-audit + pkgs.cargo-llvm-cov + pkgs.cargo-nextest + rustNightlyScript + rustToolchain + ]; +} diff --git a/src/libxrpl/peerfinder/Config.cpp b/src/libxrpl/peerfinder/Config.cpp index 60ac0ca547..2e0f793a3a 100644 --- a/src/libxrpl/peerfinder/Config.cpp +++ b/src/libxrpl/peerfinder/Config.cpp @@ -107,8 +107,12 @@ Config::makeConfig( else { config.outPeers = *limits.outPeers; - config.inPeers = *limits.inPeers; - config.maxPeers = 0; + + // Inbound slots only exist if we accept incoming connections, and + // `maxPeers` is the total across both directions. The legacy branch + // above upholds the same two invariants. + config.inPeers = config.wantIncoming ? *limits.inPeers : 0; + config.maxPeers = config.inPeers + config.outPeers; } // This will cause servers configured as validators to request that diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index ecd4832928..99ae066475 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -6,9 +6,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -397,6 +399,59 @@ checkEncryptedAmountFormat(STObject const& object) return tesSUCCESS; } +bool +isIssuerMirrorCurrent(SLE const& issuance, SLE const& mptoken) +{ + XRPL_ASSERT( + issuance.getType() == ltMPTOKEN_ISSUANCE, + "xrpl::isIssuerMirrorCurrent : issuance MPTokenIssuance object"); + XRPL_ASSERT( + mptoken.getType() == ltMPTOKEN, "xrpl::isIssuerMirrorCurrent : mptoken MPToken object"); + + return mptoken.isFieldPresent(sfIssuerEncryptedBalance) && + mptoken[~sfIssuerKeyMirrorEpoch].value_or(0) == issuance[~sfIssuerKeyEpoch].value_or(0); +} + +bool +isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken) +{ + XRPL_ASSERT( + issuance.getType() == ltMPTOKEN_ISSUANCE, + "xrpl::isAuditorMirrorCurrent : issuance MPTokenIssuance object"); + XRPL_ASSERT( + mptoken.getType() == ltMPTOKEN, "xrpl::isAuditorMirrorCurrent : mptoken MPToken object"); + + if (!issuance.isFieldPresent(sfAuditorEncryptionKey)) + return true; + + return mptoken.isFieldPresent(sfAuditorEncryptedBalance) && + mptoken[~sfAuditorKeyMirrorEpoch].value_or(0) == issuance[~sfAuditorKeyEpoch].value_or(0); +} + +bool +areMirrorsCurrent(SLE const& issuance, SLE const& mptoken) +{ + return isIssuerMirrorCurrent(issuance, mptoken) && isAuditorMirrorCurrent(issuance, mptoken); +} + +void +setMirrorEpochs(SLE const& issuance, SLE& mptoken) +{ + XRPL_ASSERT( + issuance.getType() == ltMPTOKEN_ISSUANCE, + "xrpl::setMirrorEpochs : issuance MPTokenIssuance object"); + XRPL_ASSERT(mptoken.getType() == ltMPTOKEN, "xrpl::setMirrorEpochs : mptoken MPToken object"); + + if (auto const epoch = issuance[~sfIssuerKeyEpoch].value_or(0); epoch != 0) + mptoken[sfIssuerKeyMirrorEpoch] = epoch; + + if (mptoken.isFieldPresent(sfAuditorEncryptedBalance)) + { + if (auto const epoch = issuance[~sfAuditorKeyEpoch].value_or(0); epoch != 0) + mptoken[sfAuditorKeyMirrorEpoch] = epoch; + } +} + TER verifySchnorrProof(Slice const& pubKeySlice, Slice const& proofSlice, uint256 const& contextHash) { diff --git a/src/libxrpl/protocol/STValidation.cpp b/src/libxrpl/protocol/STValidation.cpp index 1656aad3a2..9fdb6e4cee 100644 --- a/src/libxrpl/protocol/STValidation.cpp +++ b/src/libxrpl/protocol/STValidation.cpp @@ -59,6 +59,10 @@ STValidation::validationFormat() {sfBaseFeeDrops, SoeOptional}, {sfReserveBaseDrops, SoeOptional}, {sfReserveIncrementDrops, SoeOptional}, + // featureSmartEscrow + {sfGasLimit, SoeOptional}, + {sfBytecodeSizeLimit, SoeOptional}, + {sfGasPrice, SoeOptional}, }; // clang-format on diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index c2167d58ce..c6ebe98642 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -108,6 +108,8 @@ transResults() MAKE_ERROR(tecPRECISION_LOSS, "The amounts used by the transaction cannot interact."), MAKE_ERROR(tecBAD_PROOF, "Proof cannot be verified"), MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."), + MAKE_ERROR(tecOUT_OF_GAS, "The WASM code ran out of gas during execution."), + MAKE_ERROR(tecBYTECODE_REJECTED, "The custom WASM code that was run rejected your transaction."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), @@ -133,6 +135,8 @@ transResults() MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."), MAKE_ERROR(tefNO_DST_PARTIAL, "Partial payment to create account not allowed."), MAKE_ERROR(tefBAD_PATH_COUNT, "Malformed: Too many paths."), + MAKE_ERROR(tefNO_BYTECODE, "There is no WASM code to run, but a WASM-specific field was included."), + MAKE_ERROR(tefBYTECODE_NOT_INCLUDED, "WASM code requires a field that was not included."), MAKE_ERROR(telLOCAL_ERROR, "Local failure."), MAKE_ERROR(telBAD_DOMAIN, "Domain too long."), @@ -204,6 +208,8 @@ transResults() MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."), MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."), MAKE_ERROR(temBAD_CIPHERTEXT, "Malformed: Invalid ciphertext."), + MAKE_ERROR(temINVALID_BYTECODE, "Malformed: Provided byte code is invalid."), + MAKE_ERROR(temTEMP_DISABLED, "The transaction requires logic that is currently temporarily disabled."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp index 0fe27fb3ba..589ad70230 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp @@ -88,8 +88,13 @@ EscrowCreate::checkExtraFeatures(PreflightContext const& ctx) // Only require featureMPTokensV1 when the escrow amount is an MPT and // fixCleanup3_2_0 is active; XRP/IOU escrows are unaffected by this gate. if (ctx.rules.enabled(fixCleanup3_2_0) && ctx.tx[sfAmount].holds()) - return ctx.rules.enabled(featureMPTokensV1); - return true; + { + if (!ctx.rules.enabled(featureMPTokensV1)) + return false; + } + + return (!ctx.tx.isFieldPresent(sfBytecode) && !ctx.tx.isFieldPresent(sfData)) || + ctx.rules.enabled(featureSmartEscrow); } template diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp index 09219b0bf1..59bec8ca4c 100644 --- a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp @@ -65,7 +65,13 @@ checkCondition(Slice f, Slice c) bool EscrowFinish::checkExtraFeatures(PreflightContext const& ctx) { - return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials); + if (ctx.tx.isFieldPresent(sfCredentialIDs) && !ctx.rules.enabled(featureCredentials)) + return false; + + if (ctx.tx.isFieldPresent(sfGas) && !ctx.rules.enabled(featureSmartEscrow)) + return false; + + return true; } NotTEC diff --git a/src/libxrpl/tx/transactors/system/Change.cpp b/src/libxrpl/tx/transactors/system/Change.cpp index f27855a5c8..0d50b80af4 100644 --- a/src/libxrpl/tx/transactors/system/Change.cpp +++ b/src/libxrpl/tx/transactors/system/Change.cpp @@ -123,6 +123,12 @@ Change::preclaim(PreclaimContext const& ctx) ctx.tx.isFieldPresent(sfReserveIncrementDrops)) return temDISABLED; } + // The ttFEE transaction format defines these fields as optional, + // but they are unconditionally forbidden until FeeVoteImpl is + // updated to populate them (SmartEscrow behavioral port). + if (ctx.tx.isFieldPresent(sfGasLimit) || ctx.tx.isFieldPresent(sfBytecodeSizeLimit) || + ctx.tx.isFieldPresent(sfGasPrice)) + return temDISABLED; return tesSUCCESS; case ttAMENDMENT: case ttUNL_MODIFY: diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp index 19ec99702a..b3bd276e5f 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTClawback.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -209,6 +210,12 @@ ConfidentialMPTClawback::doApply() (*sleHolderMPToken)[sfAuditorEncryptedBalance] = std::move(*encZeroForAuditor); } + // Allow clawback on stale mirrors since the issuer can still generate the + // proof using the corresponding stale private key. The mirrors are updated + // to the current epoch during execution. + if (view().rules().enabled(featureConfidentialMPTKeyRotation)) + setMirrorEpochs(*sleIssuance, *sleHolderMPToken); + // Decrease Global Confidential Outstanding Amount auto const oldCOA = (*sleIssuance)[sfConfidentialOutstandingAmount]; if (clawAmount > oldCOA) diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp index 5be3892151..0855f3230d 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvert.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -111,6 +112,17 @@ ConfidentialMPTConvert::preclaim(PreclaimContext const& ctx) if (!sleMptoken) return tecOBJECT_NOT_FOUND; + // An already-initialized holder has their new ciphertexts homomorphically + // added to their existing mirrors, so those mirrors must be encrypted under + // the currently registered keys. A first-time convert creates the mirrors + // under those keys instead, and has nothing to be stale. + if (ctx.view.rules().enabled(featureConfidentialMPTKeyRotation) && + sleMptoken->isFieldPresent(sfIssuerEncryptedBalance) && + !areMirrorsCurrent(*sleIssuance, *sleMptoken)) + { + return tecNO_PERMISSION; + } + auto const mptIssue = MPTIssue{issuanceID}; // Explicit freeze and auth checks are required because accountHolds @@ -331,6 +343,10 @@ ConfidentialMPTConvert::doApply() if (auditorEc) (*sleMptoken)[sfAuditorEncryptedBalance] = *auditorEc; + // Initialize key epochs when registering the keys. + if (view().rules().enabled(featureConfidentialMPTKeyRotation)) + setMirrorEpochs(*sleIssuance, *sleMptoken); + // Spending balance starts at zero. Must use canonical zero encryption // (deterministic ciphertext) so the ledger state is reproducible. auto zeroBalance = encryptCanonicalZeroAmount( diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp index 1e3617ffbd..0c9b5d3345 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTConvertBack.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -197,6 +198,14 @@ ConfidentialMPTConvertBack::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } + // Converting back homomorphically subtracts from the holder's mirrors, so + // those mirrors must be current. + if (ctx.view.rules().enabled(featureConfidentialMPTKeyRotation) && + !areMirrorsCurrent(*sleIssuance, *sleMptoken)) + { + return tecNO_PERMISSION; + } + // Sanity check: holder's MPToken must have auditor balance field if auditing // is enabled if (requiresAuditor && !sleMptoken->isFieldPresent(sfAuditorEncryptedBalance)) diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp index e713ae5029..14ab874102 100644 --- a/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTSend.cpp @@ -247,6 +247,15 @@ ConfidentialMPTSend::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } + // A send homomorphically updates the mirrors of both parties, so both must + // be current. + if (ctx.view.rules().enabled(featureConfidentialMPTKeyRotation) && + (!areMirrorsCurrent(*sleIssuance, *sleSenderMPToken) || + !areMirrorsCurrent(*sleIssuance, *sleDestinationMPToken))) + { + return tecNO_PERMISSION; + } + // Sanity check: Both MPTokens' auditor fields must be present if auditing // is enabled if (requiresAuditor && diff --git a/src/test/app/ConfidentialMPTKeyRotation_test.cpp b/src/test/app/ConfidentialMPTKeyRotation_test.cpp index c4e8e607da..3f0f64d89d 100644 --- a/src/test/app/ConfidentialMPTKeyRotation_test.cpp +++ b/src/test/app/ConfidentialMPTKeyRotation_test.cpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace xrpl { @@ -604,6 +605,661 @@ class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase BEAST_EXPECT(mptAlice.checkKeyEpochs(kMaxKeyEpoch, kMaxKeyEpoch)); } + void + testConfidentialMPTConvertEpoch(FeatureBitset features) + { + testcase("ConfidentialMPTConvert mirror epoch"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const auditor("auditor"); + + // A first-time convert with no rotation leaves both mirror + // epochs absent. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor}); + setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor}); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt)); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + + // Both mirrors are current, so converting again is allowed and + // leaves the epochs untouched. + mptAlice.convert({ + .account = bob, + .amt = 20, + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + } + + // Every remaining case needs key rotation to be enabled. + if (!features[featureConfidentialMPTKeyRotation]) + return; + + // A first-time convert stamps the mirrors with whatever epochs the + // issuance currently sits at. Only issuer key rotated in this case. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor}); + setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor}); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + // Ten rotations, issuance's issuer epoch is 10. + for (int i = 0; i < 10; ++i) + { + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + } + + BEAST_EXPECT(mptAlice.checkKeyEpochs(10u, std::nullopt)); + BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor)); + + // carol converts for the first time, and her mirrors are stamped with the current + // issuer epoch of 10. + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 10u, std::nullopt)); + } + + // A first-time convert stamps the mirrors with whatever epochs the + // issuance currently sits at. Both keys rotated in this case. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor}); + setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor}); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + // 100 rotations of both keys, so both epochs are 100. + for (int i = 0; i < 100; ++i) + { + mptAlice.generateKeyPair(alice); + mptAlice.generateKeyPair(auditor); + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + } + + // 5 more rotations of the auditor key alone, so the auditor epoch is 105 now. + for (int i = 0; i < 5; ++i) + { + mptAlice.generateKeyPair(auditor); + mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)}); + } + + BEAST_EXPECT(mptAlice.checkKeyEpochs(100u, 105u)); + BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor)); + + // carol converts for the first time, and each of her mirrors is stamped with the epoch + // of the key it was encrypted under. + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 100u, 105u)); + } + + // An issuer key rotation leaves an existing holder's issuer mirror + // behind, converting will be blocked until the holder's mirror is updated to the new epoch. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor}); + setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor}); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + // carol initializes before any rotation, so her mirrors carry no epoch + // at all, the state every holder is in before the amendment. + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt)); + + // Rotate the issuer key to epoch 1. + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt)); + + // bob converts for the first time which is allowed when registering the key. + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt)); + + // carol's absent epoch reads as 0 which is stale. + mptAlice.convert({ + .account = carol, + .amt = 20, + .err = tecNO_PERMISSION, + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt)); + + // Rotate the issuer key to epoch 2, leaving bob's issuer mirror stale. + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(2u, std::nullopt)); + + // This is not the first time convert, and bob's issuer mirror is behind the current + // epoch, so the convert is rejected. + mptAlice.convert({ + .account = bob, + .amt = 20, + .err = tecNO_PERMISSION, + }); + + // The rejected convert leaves bob's mirrors exactly as they were. + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt)); + + // carol still cannot convert. + mptAlice.convert({ + .account = carol, + .amt = 20, + .err = tecNO_PERMISSION, + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt)); + } + + // The auditor mirror is checked the same way, so rotating only the + // auditor key blocks the convert on its own, with the issuer epoch + // untouched. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor}); + setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor}); + + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + // bob initializes his confidential balance at epoch 0, so both of his + // mirrors are current. + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + + // Rotate the auditor key only, leaving bob's auditor mirror behind + // while his issuer mirror stays current. + mptAlice.generateKeyPair(auditor); + mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)}); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u)); + BEAST_EXPECT(mptAlice.checkEncryptionKeys(alice, auditor)); + + mptAlice.convert({ + .account = bob, + .amt = 20, + .err = tecNO_PERMISSION, + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + + // Carol converts for the first time, and her auditor mirror is stamped with the current + // auditor epoch of 1. + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, 1u)); + } + + // A late auditor key registration bumps no epoch. + // Although both epochs are still zero, the convert is blocked + // because auditor mirror is missing. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor}); + setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor}); + + // Register the issuer key only. + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + }); + + // The issuance has no auditor yet, so no auditor mirror is created. + mptAlice.convert({ + .account = bob, + .amt = 50, + .fillAuditorEncryptedAmt = false, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + + // Register the auditor key later, which bumps no epoch. + mptAlice.set({ + .account = alice, + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, std::nullopt)); + + // bob's auditor mirror is still missing, so the convert is rejected. + mptAlice.convert({ + .account = bob, + .amt = 20, + .err = tecNO_PERMISSION, + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + } + } + + void + testConfidentialMPTSendEpoch(FeatureBitset features) + { + testcase("ConfidentialMPTSend mirror epoch"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const auditor("auditor"); + + // Two holders that both initialized after a rotation are current, so a + // send between them succeeds and leaves both mirrors untouched. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + setupConfidentialIssuance(mptAlice, alice, {bob, carol}); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // Rotate the issuer key to epoch 1 before anyone holds a confidential + // balance. + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt)); + + for (auto const& holder : {bob, carol}) + { + mptAlice.convert({ + .account = holder, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(holder), + }); + mptAlice.mergeInbox({.account = holder}); + } + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt)); + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 1u, std::nullopt)); + + mptAlice.send({.account = bob, .dest = carol, .amt = 10}); + + // The epochs are unchanged after send. + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt)); + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, 1u, std::nullopt)); + } + + // Either the sender or the destination being stale will be rejected. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + setupConfidentialIssuance(mptAlice, alice, {bob, carol}); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // carol initializes at epoch 0. + mptAlice.convert({ + .account = carol, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(carol), + }); + mptAlice.mergeInbox({.account = carol}); + + // Rotate the issuer key to epoch 1, leaving carol behind. + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // bob initializes after the rotation, so his mirrors are current. + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + mptAlice.mergeInbox({.account = bob}); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt)); + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt)); + + // This is rejected because the sender is the stale even though the destination is + // current. + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + + // This is rejected because the destination is the stale even though the sender is + // current. + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .err = tecNO_PERMISSION, + }); + + // The rejected sends leave both mirrors as they were. + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt)); + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt)); + } + + // Auditor mirror is stale, the send will be rejected. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob, carol}, .auditor = auditor}); + setupConfidentialIssuance(mptAlice, alice, {bob, carol}, {auditor}); + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + for (auto const& holder : {bob, carol}) + { + mptAlice.convert({ + .account = holder, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(holder), + }); + mptAlice.mergeInbox({.account = holder}); + } + + // Rotate the auditor key only + mptAlice.generateKeyPair(auditor); + mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)}); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u)); + + mptAlice.send({ + .account = bob, + .dest = carol, + .amt = 10, + .err = tecNO_PERMISSION, + }); + + mptAlice.send({ + .account = carol, + .dest = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + BEAST_EXPECT(mptAlice.checkMirrorEpochs(carol, std::nullopt, std::nullopt)); + } + } + + void + testConfidentialMPTConvertBackEpoch(FeatureBitset features) + { + testcase("ConfidentialMPTConvertBack mirror epoch"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + + // A holder who initialized after a rotation is current, so converting + // back is allowed and leaves the epoch it was stamped with alone. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + setupConfidentialIssuance(mptAlice, alice, {bob}); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // Rotate the issuer key to epoch 1 before bob holds a confidential + // balance. + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt)); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + mptAlice.mergeInbox({.account = bob}); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt)); + + mptAlice.convertBack({.account = bob, .amt = 20}); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 1u, std::nullopt)); + } + + // Converting back with stale mirrors is rejected. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + setupConfidentialIssuance(mptAlice, alice, {bob}); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // bob initializes at epoch 0. + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + mptAlice.mergeInbox({.account = bob}); + + // Converting back is allowed while his mirrors are still current. + mptAlice.convertBack({.account = bob, .amt = 20}); + + // Rotate the issuer key to epoch 1, leaving bob behind. + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.convertBack({ + .account = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + + // The rejected convert back leaves bob's mirrors as they were. + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + } + + // Converting back with a stale auditor mirror is rejected, even if the issuer mirror is + // current. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor}); + setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor}); + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + mptAlice.mergeInbox({.account = bob}); + + // Rotate the auditor key only, leaving bob behind on that mirror alone. + mptAlice.generateKeyPair(auditor); + mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)}); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 1u)); + + mptAlice.convertBack({ + .account = bob, + .amt = 10, + .err = tecNO_PERMISSION, + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + } + } + + void + testConfidentialMPTClawbackEpoch(FeatureBitset features) + { + testcase("ConfidentialMPTClawback mirror epoch"); + using namespace test::jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + + std::uint32_t const clawbackFlags = + tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance | tfMPTCanClawback; + + // Clawback is not blocked on + // a stale auditor mirror. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}, .auditor = auditor}); + setupConfidentialIssuance(mptAlice, alice, {bob}, {auditor}, clawbackFlags); + mptAlice.set({ + .account = alice, + .issuerPubKey = mptAlice.getPubKey(alice), + .auditorPubKey = mptAlice.getPubKey(auditor), + }); + + // bob initializes both mirrors at epoch 0. + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + // Rotate the auditor key twice, leaving bob's auditor mirror behind. + for (int i = 0; i < 2; ++i) + { + mptAlice.generateKeyPair(auditor); + mptAlice.set({.account = alice, .auditorPubKey = mptAlice.getPubKey(auditor)}); + } + + BEAST_EXPECT(mptAlice.checkKeyEpochs(std::nullopt, 2u)); + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + + mptAlice.confidentialClaw({.account = alice, .holder = bob, .amt = 50}); + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, 2u)); + } + + // A holder who initialized after a rotation is clawed back successfully, and + // the issuer mirror is updated to the current epoch. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + setupConfidentialIssuance(mptAlice, alice, {bob}, {}, clawbackFlags); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // Rotate the issuer key five times, issuance's issuer epoch is 5. + for (int i = 0; i < 5; ++i) + { + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + } + + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 5u, std::nullopt)); + + mptAlice.confidentialClaw({.account = alice, .holder = bob, .amt = 50}); + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, 5u, std::nullopt)); + } + + // Clawback is not blocked on + // a stale issuer mirror. For now the proof cannot verify: it is checked + // against the key registered on the issuance, while the mirror is still + // encrypted under the key it was written with, and that older key is + // nowhere on the ledger yet. This will be added in a separate PR. + { + Env env{*this, features}; + MPTTester mptAlice(env, alice, {.holders = {bob}}); + setupConfidentialIssuance(mptAlice, alice, {bob}, {}, clawbackFlags); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // bob initializes at epoch 0. + mptAlice.convert({ + .account = bob, + .amt = 50, + .holderPubKey = mptAlice.getPubKey(bob), + }); + + // Rotate the issuer key to epoch 1, leaving bob behind. + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + BEAST_EXPECT(mptAlice.checkKeyEpochs(1u, std::nullopt)); + + mptAlice.confidentialClaw({ + .account = alice, + .holder = bob, + .amt = 50, + .err = tecBAD_PROOF, + }); + + BEAST_EXPECT(mptAlice.checkMirrorEpochs(bob, std::nullopt, std::nullopt)); + } + } + +public: void testMPTokenIssuanceSetWithFeats(FeatureBitset features) { @@ -626,6 +1282,12 @@ public: testMPTokenIssuanceSetWithFeats(all); testMPTokenIssuanceSetWithFeats(all - featureConfidentialMPTKeyRotation); + + testConfidentialMPTConvertEpoch(all); + testConfidentialMPTConvertEpoch(all - featureConfidentialMPTKeyRotation); + testConfidentialMPTSendEpoch(all); + testConfidentialMPTConvertBackEpoch(all); + testConfidentialMPTClawbackEpoch(all); } }; diff --git a/src/test/jtx/ConfidentialTransfer.h b/src/test/jtx/ConfidentialTransfer.h index 465bac03db..02b2e8dccb 100644 --- a/src/test/jtx/ConfidentialTransfer.h +++ b/src/test/jtx/ConfidentialTransfer.h @@ -418,6 +418,18 @@ protected: } }; + // Create an issuance that can hold confidential balances, with the listed + // holders funded and authorized, and a key pair generated for the issuer, + // every holder, and every extra key owner. The keys are + // generated but not registered. + static void + setupConfidentialIssuance( + test::jtx::MPTTester& mpt, + test::jtx::Account const& issuer, + std::vector const& holders, + std::vector const& keyOwners = {}, + std::uint32_t flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance); + // Set up an MPT environment suitable for batch testing. // alice is issuer; bob has 'bobAmt' in confidential spending; carol has // 'carolAmt' in confidential spending; dave is initialised with pubkey but diff --git a/src/test/jtx/impl/ConfidentialTransfer.cpp b/src/test/jtx/impl/ConfidentialTransfer.cpp new file mode 100644 index 0000000000..6c1538316c --- /dev/null +++ b/src/test/jtx/impl/ConfidentialTransfer.cpp @@ -0,0 +1,37 @@ +#include + +#include +#include + +#include +#include + +namespace xrpl { + +void +ConfidentialTransferTestBase::setupConfidentialIssuance( + test::jtx::MPTTester& mpt, + test::jtx::Account const& issuer, + std::vector const& holders, + std::vector const& keyOwners, + std::uint32_t flags) +{ + using namespace test::jtx; + mpt.create({ + .ownerCount = 1, + .flags = flags, + }); + + for (auto const& holder : holders) + { + mpt.authorize({.account = holder}); + mpt.pay(issuer, holder, 100); + mpt.generateKeyPair(holder); + } + + mpt.generateKeyPair(issuer); + for (auto const& keyOwner : keyOwners) + mpt.generateKeyPair(keyOwner); +} + +} // namespace xrpl diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 0c1ff14eab..48183c4a2f 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -92,6 +92,33 @@ makePedersenParams(PedersenProofParams const& params) return res; } +/** + * @brief Looks up an account's key at a given key epoch. + * + * @param keys accounts' history of keys, indexed by key epoch. + * @param account The account whose key is being looked up. + * @param epoch The key epoch, or std::nullopt for the account's latest key. + * @return The key, or std::nullopt if the account has no key at that epoch. + */ +[[nodiscard]] std::optional +keyAtEpoch( + std::unordered_map> const& keys, + AccountID const& account, + std::optional epoch) +{ + auto const it = keys.find(account); + if (it == keys.end() || it->second.empty()) + return std::nullopt; + + if (!epoch) + return it->second.back(); + + if (*epoch >= it->second.size()) + return std::nullopt; + + return it->second[*epoch]; +} + } // namespace void @@ -659,6 +686,33 @@ MPTTester::checkKeyEpochs( }); } +[[nodiscard]] bool +MPTTester::checkMirrorEpochs( + Account const& holder, + std::optional issuerKeyMirrorEpoch, + std::optional auditorKeyMirrorEpoch) const +{ + return forObject( + [&](SLEP const& sle) -> bool { + return (*sle)[~sfIssuerKeyMirrorEpoch] == issuerKeyMirrorEpoch && + (*sle)[~sfAuditorKeyMirrorEpoch] == auditorKeyMirrorEpoch; + }, + holder); +} + +[[nodiscard]] std::optional +MPTTester::getMirrorEpoch(Account const& holder, SF_UINT32 const& field) const +{ + std::optional epoch; + forObject( + [&](SLEP const& sle) -> bool { + epoch = (*sle)[~field]; + return true; + }, + holder); + return epoch; +} + [[nodiscard]] bool MPTTester::checkEncryptionKeys( std::optional const& issuerKeyOwner, @@ -1172,8 +1226,13 @@ MPTTester::convert(MPTConvert const& arg) if (!prevInboxBalance || !prevSpendingBalance || !prevIssuerBalance) Throw("Failed to get Pre-convert balance"); + // The auditor mirror is only touched if the transaction carries an auditor + // ciphertext, which mirrors the condition convertJV fills it under. + bool const hasAuditorAmt = + arg.auditorEncryptedAmt || (auditor_ && arg.fillAuditorEncryptedAmt.value_or(false)); + std::optional prevAuditorBalance; - if (arg.auditorEncryptedAmt || auditor_) + if (hasAuditorAmt) { prevAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance); if (!prevAuditorBalance) @@ -1212,7 +1271,7 @@ MPTTester::convert(MPTConvert const& arg) if (!postInboxBalance || !postIssuerBalance || !postSpendingBalance) Throw("Failed to get post-convert balance"); - if (arg.auditorEncryptedAmt || auditor_) + if (hasAuditorAmt) { auto const postAuditorBalance = getDecryptedBalance(*arg.account, auditorEncryptedBalance); @@ -2039,7 +2098,7 @@ MPTTester::confidentialClaw(MPTConfidentialClawback const& arg) } } -void +std::uint32_t MPTTester::generateKeyPair(Account const& account) { unsigned char privKey[kEcPrivKeyLength]; @@ -2057,26 +2116,23 @@ MPTTester::generateKeyPair(Account const& account) Throw("failed to serialize public key"); } - pubKeys_.insert({account.id(), Buffer{compressedPubKey, kEcPubKeyLength}}); - privKeys_.insert({account.id(), Buffer{privKey, kEcPrivKeyLength}}); + auto& pubKeyEpochs = pubKeys_[account.id()]; + pubKeyEpochs.emplace_back(compressedPubKey, kEcPubKeyLength); + privKeys_[account.id()].emplace_back(privKey, kEcPrivKeyLength); + + return static_cast(pubKeyEpochs.size() - 1); } std::optional -MPTTester::getPubKey(Account const& account) const +MPTTester::getPubKey(Account const& account, std::optional epoch) const { - if (auto const it = pubKeys_.find(account.id()); it != pubKeys_.end()) - return it->second; - - return std::nullopt; + return keyAtEpoch(pubKeys_, account.id(), epoch); } std::optional -MPTTester::getPrivKey(Account const& account) const +MPTTester::getPrivKey(Account const& account, std::optional epoch) const { - if (auto const it = privKeys_.find(account.id()); it != privKeys_.end()) - return it->second; - - return std::nullopt; + return keyAtEpoch(privKeys_, account.id(), epoch); } Buffer @@ -2095,7 +2151,10 @@ MPTTester::encryptAmount(Account const& account, uint64_t const amt, Buffer cons } std::optional -MPTTester::decryptAmount(Account const& account, Buffer const& amt) const +MPTTester::decryptAmount( + Account const& account, + Buffer const& amt, + std::optional epoch) const { if (amt.size() != kEcGamalEncryptedTotalLength) return std::nullopt; @@ -2104,7 +2163,7 @@ MPTTester::decryptAmount(Account const& account, Buffer const& amt) const if (!pair) return std::nullopt; - auto const privKey = getPrivKey(account); + auto const privKey = getPrivKey(account, epoch); if (!privKey || privKey->size() != kEcPrivKeyLength) return std::nullopt; @@ -2136,18 +2195,24 @@ MPTTester::getDecryptedBalance(Account const& account, EncryptedBalanceType bala Account decryptor = account; + // A mirror stays encrypted under the key it was written with, so a rotation + // leaves it readable only by that generation of the key, not the latest one. + std::optional epoch; + if (balanceType == issuerEncryptedBalance) { decryptor = issuer_; + epoch = getMirrorEpoch(account, sfIssuerKeyMirrorEpoch).value_or(0); } else if (balanceType == auditorEncryptedBalance) { if (!auditor_) return std::nullopt; decryptor = *auditor_; + epoch = getMirrorEpoch(account, sfAuditorKeyMirrorEpoch).value_or(0); } - return decryptAmount(decryptor, *encryptedAmt); + return decryptAmount(decryptor, *encryptedAmt, epoch); }; json::Value diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index 26329ad78c..13bf4ead3f 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -451,8 +452,10 @@ class MPTTester std::optional const auditor_; std::optional id_; bool close_; - std::unordered_map pubKeys_; - std::unordered_map privKeys_; + // Keys generated for each account. Buffer vector's index is the key epoch: index 0 is + // the initial pair and each rotation appends. + std::unordered_map> pubKeys_; + std::unordered_map> privKeys_; public: enum class EncryptedBalanceType { @@ -619,6 +622,15 @@ public: std::optional issuerKeyEpoch, std::optional auditorKeyEpoch) const; + // Checks both mirror epochs on a holder's MPToken. Pass std::nullopt for an + // epoch that is expected to be absent, which means the mirror was written + // under the issuance's epoch 0 key. + [[nodiscard]] bool + checkMirrorEpochs( + Account const& holder, + std::optional issuerKeyMirrorEpoch, + std::optional auditorKeyMirrorEpoch) const; + // Checks that the issuance carries the encryption keys of the given // accounts. Pass std::nullopt for a key that is expected to be absent, // which means the key is never registered. @@ -678,20 +690,31 @@ public: operator Asset() const; - void + // Generates the account's next key pair and returns the key epoch it landed + // at, leaving the earlier ones retrievable. + std::uint32_t generateKeyPair(Account const& account); + // Returns the account's public key at the given key epoch, or its latest key when + // no epoch is given. [[nodiscard]] std::optional - getPubKey(Account const& account) const; + getPubKey(Account const& account, std::optional epoch = std::nullopt) const; + // Returns the account's private key at the given key epoch, or its latest key when + // no epoch is given. [[nodiscard]] std::optional - getPrivKey(Account const& account) const; + getPrivKey(Account const& account, std::optional epoch = std::nullopt) const; [[nodiscard]] Buffer encryptAmount(Account const& account, uint64_t const amt, Buffer const& blindingFactor) const; + // Decrypts with the account's key at the given key epoch, or its latest key + // when no epoch is given. [[nodiscard]] std::optional - decryptAmount(Account const& account, Buffer const& amt) const; + decryptAmount( + Account const& account, + Buffer const& amt, + std::optional epoch = std::nullopt) const; [[nodiscard]] std::optional getDecryptedBalance(Account const& account, EncryptedBalanceType balanceType) const; @@ -745,6 +768,10 @@ private: std::function const& cb, std::optional const& holder = std::nullopt) const; + // Reads one of the holder's mirror key epochs off their MPToken. + [[nodiscard]] std::optional + getMirrorEpoch(Account const& holder, SF_UINT32 const& field) const; + template TER submit(A const& arg, json::Value jv) diff --git a/src/test/overlay/overlay_limit_test.cpp b/src/test/overlay/overlay_limit_test.cpp new file mode 100644 index 0000000000..11bbdc2377 --- /dev/null +++ b/src/test/overlay/overlay_limit_test.cpp @@ -0,0 +1,83 @@ +#include +#include + +#include +#include + +#include +#include + +#include + +namespace xrpl::test { + +using namespace jtx; + +/** + * Tests for `Overlay::limit()`, the configured peer allowance reported once + * `OverlayImpl::start()` applies the computed `peer_finder::Config`. + * + * `ApplicationImp::fdRequired()` runs before `OverlayImpl::start()` does, so it + * always sees the peer finder manager's default-constructed configuration and + * never this value; `Overlay::limit()` instead surfaces through the PeerFinder + * property stream and other post-startup callers. + * + * `jtx::Env` runs standalone, and `ServerHandler` strips the `peer` protocol + * from every configured port under `config.standalone()`, so the peer port + * declared here is never bound and incoming connections are disabled + * throughout; every limit in this suite is an outbound-only allowance. The + * inbound cases live alongside `peer_finder::Config::makeConfig`, which takes + * the port as a parameter. + */ +class OverlayLimit_test : public beast::unit_test::Suite +{ + void + testLegacyPeersMax() + { + testcase("Legacy peers_max is reported"); + + auto config = jtx::envconfig(); + config->peersMax = 40; + + Env env(*this, std::move(config)); + BEAST_EXPECT(env.app().getOverlay().limit() == 40); + } + + void + testPerDirectionPeerLimits() + { + testcase("Per-direction peer limits are reported"); + + // With incoming connections disabled the 50 inbound slots are dropped + // and only the outbound allowance remains, so neither zero (the value + // `maxPeers` used to hold in this branch of makeConfig) nor 70 (the + // unconditional sum of both directions) is correct. + auto config = jtx::envconfig(); + config->peersInMax = 50; + config->peersOutMax = 20; + + Env env(*this, std::move(config)); + BEAST_EXPECT(env.app().getOverlay().limit() == 20); + } + + void + testDefaultConfig() + { + testcase("A default configuration reports the default limit"); + + Env env(*this); + BEAST_EXPECT(env.app().getOverlay().limit() == peer_finder::tuning::kDefaultMaxPeers); + } + + void + run() override + { + testLegacyPeersMax(); + testPerDirectionPeerLimits(); + testDefaultConfig(); + } +}; + +BEAST_DEFINE_TESTSUITE(OverlayLimit, overlay, xrpl); + +} // namespace xrpl::test diff --git a/src/test/rpc/NoRippleCheck_test.cpp b/src/test/rpc/NoRippleCheck_test.cpp index 8e719e6407..3a4ddcf5c4 100644 --- a/src/test/rpc/NoRippleCheck_test.cpp +++ b/src/test/rpc/NoRippleCheck_test.cpp @@ -126,9 +126,16 @@ class NoRippleCheck_test : public beast::unit_test::Suite params[jss::account] = toBase58(TokenType::NodePrivate, alice.sk()); params[jss::role] = "user"; params[jss::ledger] = "current"; + params[jss::transactions] = true; auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result]; BEAST_EXPECT(result[jss::error] == "actMalformed"); BEAST_EXPECT(result[jss::error_message] == "Account malformed."); + // The changelog promises malformed-account responses carry + // neither `transactions` nor any ledger metadata. + BEAST_EXPECT(!result.isMember(jss::transactions)); + BEAST_EXPECT(!result.isMember(jss::ledger_hash)); + BEAST_EXPECT(!result.isMember(jss::ledger_index)); + BEAST_EXPECT(!result.isMember(jss::validated)); } { @@ -194,6 +201,7 @@ class NoRippleCheck_test : public beast::unit_test::Suite if (!BEAST_EXPECT(pa.isArray())) return; + BEAST_EXPECT(!result.isMember(jss::transactions)); if (problems) { if (!BEAST_EXPECT(pa.size() == 2)) @@ -219,12 +227,12 @@ class NoRippleCheck_test : public beast::unit_test::Suite // time. params[jss::transactions] = true; result = env.rpc("json", "noripple_check", to_string(params))[jss::result]; - if (!BEAST_EXPECT(result[jss::transactions].isArray())) - return; auto const txs = result[jss::transactions]; if (problems) { + if (!BEAST_EXPECT(result[jss::transactions].isArray())) + return; if (!BEAST_EXPECT(txs.size() == (user ? 1 : 2))) return; diff --git a/src/tests/libxrpl/peerfinder/PeerFinder.cpp b/src/tests/libxrpl/peerfinder/PeerFinder.cpp index 3a52bbb5aa..2d98d4385f 100644 --- a/src/tests/libxrpl/peerfinder/PeerFinder.cpp +++ b/src/tests/libxrpl/peerfinder/PeerFinder.cpp @@ -1214,6 +1214,9 @@ TEST(PeerFinderConfig, applies_legacy_and_explicit_peer_limits) .expectedOut = 10, .expectedIn = 0, .expectedIpLimit = 1}, + // A port of zero disables incoming connections, so the configured + // inbound limit is dropped and the per-IP inbound limit collapses to + // one, exactly as in the legacy private case above. {.name = "new in 100/out 10, private", .maxPeers = {}, .maxIn = 100, @@ -1221,7 +1224,7 @@ TEST(PeerFinderConfig, applies_legacy_and_explicit_peer_limits) .port = 0, .expectedOut = 10, .expectedIn = 0, - .expectedIpLimit = 6}}; + .expectedIpLimit = 1}}; for (auto const& testCase : cases) { @@ -1239,6 +1242,13 @@ TEST(PeerFinderConfig, applies_legacy_and_explicit_peer_limits) EXPECT_EQ(counts.inMax(), testCase.expectedIn); EXPECT_EQ(config.ipLimit, testCase.expectedIpLimit); + // The configuration itself carries the same per-direction allowances + // that the slot counts derive, and `maxPeers` is their total. Callers + // such as `Overlay::limit` read `maxPeers` directly. + EXPECT_EQ(config.outPeers, testCase.expectedOut); + EXPECT_EQ(config.inPeers, testCase.expectedIn); + EXPECT_EQ(config.maxPeers, config.inPeers + config.outPeers); + NiceMock store; allowEmptyStore(store); NiceMock checker; diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/EscrowTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/EscrowTests.cpp index 2dbb450e28..ad5f88fd2f 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/EscrowTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/EscrowTests.cpp @@ -27,6 +27,8 @@ TEST(EscrowTests, BuilderSettersRoundTrip) auto const conditionValue = canonical_VL(); auto const cancelAfterValue = canonical_UINT32(); auto const finishAfterValue = canonical_UINT32(); + auto const bytecodeValue = canonical_VL(); + auto const dataValue = canonical_VL(); auto const sourceTagValue = canonical_UINT32(); auto const destinationTagValue = canonical_UINT32(); auto const ownerNodeValue = canonical_UINT64(); @@ -49,6 +51,8 @@ TEST(EscrowTests, BuilderSettersRoundTrip) builder.setCondition(conditionValue); builder.setCancelAfter(cancelAfterValue); builder.setFinishAfter(finishAfterValue); + builder.setBytecode(bytecodeValue); + builder.setData(dataValue); builder.setSourceTag(sourceTagValue); builder.setDestinationTag(destinationTagValue); builder.setDestinationNode(destinationNodeValue); @@ -132,6 +136,22 @@ TEST(EscrowTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasFinishAfter()); } + { + auto const& expected = bytecodeValue; + auto const actualOpt = entry.getBytecode(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfBytecode"); + EXPECT_TRUE(entry.hasBytecode()); + } + + { + auto const& expected = dataValue; + auto const actualOpt = entry.getData(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfData"); + EXPECT_TRUE(entry.hasData()); + } + { auto const& expected = sourceTagValue; auto const actualOpt = entry.getSourceTag(); @@ -192,6 +212,8 @@ TEST(EscrowTests, BuilderFromSleRoundTrip) auto const conditionValue = canonical_VL(); auto const cancelAfterValue = canonical_UINT32(); auto const finishAfterValue = canonical_UINT32(); + auto const bytecodeValue = canonical_VL(); + auto const dataValue = canonical_VL(); auto const sourceTagValue = canonical_UINT32(); auto const destinationTagValue = canonical_UINT32(); auto const ownerNodeValue = canonical_UINT64(); @@ -210,6 +232,8 @@ TEST(EscrowTests, BuilderFromSleRoundTrip) sle->at(sfCondition) = conditionValue; sle->at(sfCancelAfter) = cancelAfterValue; sle->at(sfFinishAfter) = finishAfterValue; + sle->at(sfBytecode) = bytecodeValue; + sle->at(sfData) = dataValue; sle->at(sfSourceTag) = sourceTagValue; sle->at(sfDestinationTag) = destinationTagValue; sle->at(sfOwnerNode) = ownerNodeValue; @@ -340,6 +364,32 @@ TEST(EscrowTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfFinishAfter"); } + { + auto const& expected = bytecodeValue; + + auto const fromSleOpt = entryFromSle.getBytecode(); + auto const fromBuilderOpt = entryFromBuilder.getBytecode(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfBytecode"); + expectEqualField(expected, *fromBuilderOpt, "sfBytecode"); + } + + { + auto const& expected = dataValue; + + auto const fromSleOpt = entryFromSle.getData(); + auto const fromBuilderOpt = entryFromBuilder.getData(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfData"); + expectEqualField(expected, *fromBuilderOpt, "sfData"); + } + { auto const& expected = sourceTagValue; @@ -477,6 +527,10 @@ TEST(EscrowTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getCancelAfter().has_value()); EXPECT_FALSE(entry.hasFinishAfter()); EXPECT_FALSE(entry.getFinishAfter().has_value()); + EXPECT_FALSE(entry.hasBytecode()); + EXPECT_FALSE(entry.getBytecode().has_value()); + EXPECT_FALSE(entry.hasData()); + EXPECT_FALSE(entry.getData().has_value()); EXPECT_FALSE(entry.hasSourceTag()); EXPECT_FALSE(entry.getSourceTag().has_value()); EXPECT_FALSE(entry.hasDestinationTag()); diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/FeeSettingsTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/FeeSettingsTests.cpp index 479d0c56c6..92bf25398c 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/FeeSettingsTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/FeeSettingsTests.cpp @@ -27,6 +27,9 @@ TEST(FeeSettingsTests, BuilderSettersRoundTrip) auto const baseFeeDropsValue = canonical_AMOUNT(); auto const reserveBaseDropsValue = canonical_AMOUNT(); auto const reserveIncrementDropsValue = canonical_AMOUNT(); + auto const gasLimitValue = canonical_UINT32(); + auto const bytecodeSizeLimitValue = canonical_UINT32(); + auto const gasPriceValue = canonical_UINT32(); auto const previousTxnIDValue = canonical_UINT256(); auto const previousTxnLgrSeqValue = canonical_UINT32(); @@ -40,6 +43,9 @@ TEST(FeeSettingsTests, BuilderSettersRoundTrip) builder.setBaseFeeDrops(baseFeeDropsValue); builder.setReserveBaseDrops(reserveBaseDropsValue); builder.setReserveIncrementDrops(reserveIncrementDropsValue); + builder.setGasLimit(gasLimitValue); + builder.setBytecodeSizeLimit(bytecodeSizeLimitValue); + builder.setGasPrice(gasPriceValue); builder.setPreviousTxnID(previousTxnIDValue); builder.setPreviousTxnLgrSeq(previousTxnLgrSeqValue); @@ -108,6 +114,30 @@ TEST(FeeSettingsTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasReserveIncrementDrops()); } + { + auto const& expected = gasLimitValue; + auto const actualOpt = entry.getGasLimit(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfGasLimit"); + EXPECT_TRUE(entry.hasGasLimit()); + } + + { + auto const& expected = bytecodeSizeLimitValue; + auto const actualOpt = entry.getBytecodeSizeLimit(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfBytecodeSizeLimit"); + EXPECT_TRUE(entry.hasBytecodeSizeLimit()); + } + + { + auto const& expected = gasPriceValue; + auto const actualOpt = entry.getGasPrice(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfGasPrice"); + EXPECT_TRUE(entry.hasGasPrice()); + } + { auto const& expected = previousTxnIDValue; auto const actualOpt = entry.getPreviousTxnID(); @@ -144,6 +174,9 @@ TEST(FeeSettingsTests, BuilderFromSleRoundTrip) auto const baseFeeDropsValue = canonical_AMOUNT(); auto const reserveBaseDropsValue = canonical_AMOUNT(); auto const reserveIncrementDropsValue = canonical_AMOUNT(); + auto const gasLimitValue = canonical_UINT32(); + auto const bytecodeSizeLimitValue = canonical_UINT32(); + auto const gasPriceValue = canonical_UINT32(); auto const previousTxnIDValue = canonical_UINT256(); auto const previousTxnLgrSeqValue = canonical_UINT32(); @@ -156,6 +189,9 @@ TEST(FeeSettingsTests, BuilderFromSleRoundTrip) sle->at(sfBaseFeeDrops) = baseFeeDropsValue; sle->at(sfReserveBaseDrops) = reserveBaseDropsValue; sle->at(sfReserveIncrementDrops) = reserveIncrementDropsValue; + sle->at(sfGasLimit) = gasLimitValue; + sle->at(sfBytecodeSizeLimit) = bytecodeSizeLimitValue; + sle->at(sfGasPrice) = gasPriceValue; sle->at(sfPreviousTxnID) = previousTxnIDValue; sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; @@ -259,6 +295,45 @@ TEST(FeeSettingsTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfReserveIncrementDrops"); } + { + auto const& expected = gasLimitValue; + + auto const fromSleOpt = entryFromSle.getGasLimit(); + auto const fromBuilderOpt = entryFromBuilder.getGasLimit(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfGasLimit"); + expectEqualField(expected, *fromBuilderOpt, "sfGasLimit"); + } + + { + auto const& expected = bytecodeSizeLimitValue; + + auto const fromSleOpt = entryFromSle.getBytecodeSizeLimit(); + auto const fromBuilderOpt = entryFromBuilder.getBytecodeSizeLimit(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfBytecodeSizeLimit"); + expectEqualField(expected, *fromBuilderOpt, "sfBytecodeSizeLimit"); + } + + { + auto const& expected = gasPriceValue; + + auto const fromSleOpt = entryFromSle.getGasPrice(); + auto const fromBuilderOpt = entryFromBuilder.getGasPrice(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfGasPrice"); + expectEqualField(expected, *fromBuilderOpt, "sfGasPrice"); + } + { auto const& expected = previousTxnIDValue; @@ -351,6 +426,12 @@ TEST(FeeSettingsTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getReserveBaseDrops().has_value()); EXPECT_FALSE(entry.hasReserveIncrementDrops()); EXPECT_FALSE(entry.getReserveIncrementDrops().has_value()); + EXPECT_FALSE(entry.hasGasLimit()); + EXPECT_FALSE(entry.getGasLimit().has_value()); + EXPECT_FALSE(entry.hasBytecodeSizeLimit()); + EXPECT_FALSE(entry.getBytecodeSizeLimit().has_value()); + EXPECT_FALSE(entry.hasGasPrice()); + EXPECT_FALSE(entry.getGasPrice().has_value()); EXPECT_FALSE(entry.hasPreviousTxnID()); EXPECT_FALSE(entry.getPreviousTxnID().has_value()); EXPECT_FALSE(entry.hasPreviousTxnLgrSeq()); diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp index 7db4b638a7..41eff1cce7 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp @@ -32,6 +32,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) auto const confidentialBalanceVersionValue = canonical_UINT32(); auto const issuerEncryptedBalanceValue = canonical_VL(); auto const auditorEncryptedBalanceValue = canonical_VL(); + auto const issuerKeyMirrorEpochValue = canonical_UINT32(); + auto const auditorKeyMirrorEpochValue = canonical_UINT32(); auto const holderEncryptionKeyValue = canonical_VL(); MPTokenBuilder builder{ @@ -49,6 +51,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) builder.setConfidentialBalanceVersion(confidentialBalanceVersionValue); builder.setIssuerEncryptedBalance(issuerEncryptedBalanceValue); builder.setAuditorEncryptedBalance(auditorEncryptedBalanceValue); + builder.setIssuerKeyMirrorEpoch(issuerKeyMirrorEpochValue); + builder.setAuditorKeyMirrorEpoch(auditorKeyMirrorEpochValue); builder.setHolderEncryptionKey(holderEncryptionKeyValue); builder.setLedgerIndex(index); @@ -146,6 +150,22 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasAuditorEncryptedBalance()); } + { + auto const& expected = issuerKeyMirrorEpochValue; + auto const actualOpt = entry.getIssuerKeyMirrorEpoch(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfIssuerKeyMirrorEpoch"); + EXPECT_TRUE(entry.hasIssuerKeyMirrorEpoch()); + } + + { + auto const& expected = auditorKeyMirrorEpochValue; + auto const actualOpt = entry.getAuditorKeyMirrorEpoch(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfAuditorKeyMirrorEpoch"); + EXPECT_TRUE(entry.hasAuditorKeyMirrorEpoch()); + } + { auto const& expected = holderEncryptionKeyValue; auto const actualOpt = entry.getHolderEncryptionKey(); @@ -179,6 +199,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) auto const confidentialBalanceVersionValue = canonical_UINT32(); auto const issuerEncryptedBalanceValue = canonical_VL(); auto const auditorEncryptedBalanceValue = canonical_VL(); + auto const issuerKeyMirrorEpochValue = canonical_UINT32(); + auto const auditorKeyMirrorEpochValue = canonical_UINT32(); auto const holderEncryptionKeyValue = canonical_VL(); auto sle = std::make_shared(MPToken::entryType, index); @@ -195,6 +217,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) sle->at(sfConfidentialBalanceVersion) = confidentialBalanceVersionValue; sle->at(sfIssuerEncryptedBalance) = issuerEncryptedBalanceValue; sle->at(sfAuditorEncryptedBalance) = auditorEncryptedBalanceValue; + sle->at(sfIssuerKeyMirrorEpoch) = issuerKeyMirrorEpochValue; + sle->at(sfAuditorKeyMirrorEpoch) = auditorKeyMirrorEpochValue; sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue; MPTokenBuilder builderFromSle{sle}; @@ -347,6 +371,32 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptedBalance"); } + { + auto const& expected = issuerKeyMirrorEpochValue; + + auto const fromSleOpt = entryFromSle.getIssuerKeyMirrorEpoch(); + auto const fromBuilderOpt = entryFromBuilder.getIssuerKeyMirrorEpoch(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfIssuerKeyMirrorEpoch"); + expectEqualField(expected, *fromBuilderOpt, "sfIssuerKeyMirrorEpoch"); + } + + { + auto const& expected = auditorKeyMirrorEpochValue; + + auto const fromSleOpt = entryFromSle.getAuditorKeyMirrorEpoch(); + auto const fromBuilderOpt = entryFromBuilder.getAuditorKeyMirrorEpoch(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfAuditorKeyMirrorEpoch"); + expectEqualField(expected, *fromBuilderOpt, "sfAuditorKeyMirrorEpoch"); + } + { auto const& expected = holderEncryptionKeyValue; @@ -436,6 +486,10 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getIssuerEncryptedBalance().has_value()); EXPECT_FALSE(entry.hasAuditorEncryptedBalance()); EXPECT_FALSE(entry.getAuditorEncryptedBalance().has_value()); + EXPECT_FALSE(entry.hasIssuerKeyMirrorEpoch()); + EXPECT_FALSE(entry.getIssuerKeyMirrorEpoch().has_value()); + EXPECT_FALSE(entry.hasAuditorKeyMirrorEpoch()); + EXPECT_FALSE(entry.getAuditorKeyMirrorEpoch().has_value()); EXPECT_FALSE(entry.hasHolderEncryptionKey()); EXPECT_FALSE(entry.getHolderEncryptionKey().has_value()); } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/EscrowCreateTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/EscrowCreateTests.cpp index 1d62fadb20..44e3e200d8 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/EscrowCreateTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/EscrowCreateTests.cpp @@ -30,11 +30,13 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip) // Transaction-specific field values auto const destinationValue = canonical_ACCOUNT(); + auto const destinationTagValue = canonical_UINT32(); auto const amountValue = canonical_AMOUNT(); auto const conditionValue = canonical_VL(); auto const cancelAfterValue = canonical_UINT32(); auto const finishAfterValue = canonical_UINT32(); - auto const destinationTagValue = canonical_UINT32(); + auto const bytecodeValue = canonical_VL(); + auto const dataValue = canonical_VL(); EscrowCreateBuilder builder{ accountValue, @@ -45,10 +47,12 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip) }; // Set optional fields + builder.setDestinationTag(destinationTagValue); builder.setCondition(conditionValue); builder.setCancelAfter(cancelAfterValue); builder.setFinishAfter(finishAfterValue); - builder.setDestinationTag(destinationTagValue); + builder.setBytecode(bytecodeValue); + builder.setData(dataValue); auto tx = builder.build(publicKey, secretKey); @@ -78,6 +82,14 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip) } // Verify optional fields + { + auto const& expected = destinationTagValue; + auto const actualOpt = tx.getDestinationTag(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present"; + expectEqualField(expected, *actualOpt, "sfDestinationTag"); + EXPECT_TRUE(tx.hasDestinationTag()); + } + { auto const& expected = conditionValue; auto const actualOpt = tx.getCondition(); @@ -103,11 +115,19 @@ TEST(TransactionsEscrowCreateTests, BuilderSettersRoundTrip) } { - auto const& expected = destinationTagValue; - auto const actualOpt = tx.getDestinationTag(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present"; - expectEqualField(expected, *actualOpt, "sfDestinationTag"); - EXPECT_TRUE(tx.hasDestinationTag()); + auto const& expected = bytecodeValue; + auto const actualOpt = tx.getBytecode(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecode should be present"; + expectEqualField(expected, *actualOpt, "sfBytecode"); + EXPECT_TRUE(tx.hasBytecode()); + } + + { + auto const& expected = dataValue; + auto const actualOpt = tx.getData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfData should be present"; + expectEqualField(expected, *actualOpt, "sfData"); + EXPECT_TRUE(tx.hasData()); } } @@ -127,11 +147,13 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip) // Transaction-specific field values auto const destinationValue = canonical_ACCOUNT(); + auto const destinationTagValue = canonical_UINT32(); auto const amountValue = canonical_AMOUNT(); auto const conditionValue = canonical_VL(); auto const cancelAfterValue = canonical_UINT32(); auto const finishAfterValue = canonical_UINT32(); - auto const destinationTagValue = canonical_UINT32(); + auto const bytecodeValue = canonical_VL(); + auto const dataValue = canonical_VL(); // Build an initial transaction EscrowCreateBuilder initialBuilder{ @@ -142,10 +164,12 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip) feeValue }; + initialBuilder.setDestinationTag(destinationTagValue); initialBuilder.setCondition(conditionValue); initialBuilder.setCancelAfter(cancelAfterValue); initialBuilder.setFinishAfter(finishAfterValue); - initialBuilder.setDestinationTag(destinationTagValue); + initialBuilder.setBytecode(bytecodeValue); + initialBuilder.setData(dataValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -176,6 +200,13 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip) } // Verify optional fields + { + auto const& expected = destinationTagValue; + auto const actualOpt = rebuiltTx.getDestinationTag(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present"; + expectEqualField(expected, *actualOpt, "sfDestinationTag"); + } + { auto const& expected = conditionValue; auto const actualOpt = rebuiltTx.getCondition(); @@ -198,10 +229,17 @@ TEST(TransactionsEscrowCreateTests, BuilderFromStTxRoundTrip) } { - auto const& expected = destinationTagValue; - auto const actualOpt = rebuiltTx.getDestinationTag(); - ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDestinationTag should be present"; - expectEqualField(expected, *actualOpt, "sfDestinationTag"); + auto const& expected = bytecodeValue; + auto const actualOpt = rebuiltTx.getBytecode(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecode should be present"; + expectEqualField(expected, *actualOpt, "sfBytecode"); + } + + { + auto const& expected = dataValue; + auto const actualOpt = rebuiltTx.getData(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfData should be present"; + expectEqualField(expected, *actualOpt, "sfData"); } } @@ -263,14 +301,18 @@ TEST(TransactionsEscrowCreateTests, OptionalFieldsReturnNullopt) auto tx = builder.build(publicKey, secretKey); // Verify optional fields are not present + EXPECT_FALSE(tx.hasDestinationTag()); + EXPECT_FALSE(tx.getDestinationTag().has_value()); EXPECT_FALSE(tx.hasCondition()); EXPECT_FALSE(tx.getCondition().has_value()); EXPECT_FALSE(tx.hasCancelAfter()); EXPECT_FALSE(tx.getCancelAfter().has_value()); EXPECT_FALSE(tx.hasFinishAfter()); EXPECT_FALSE(tx.getFinishAfter().has_value()); - EXPECT_FALSE(tx.hasDestinationTag()); - EXPECT_FALSE(tx.getDestinationTag().has_value()); + EXPECT_FALSE(tx.hasBytecode()); + EXPECT_FALSE(tx.getBytecode().has_value()); + EXPECT_FALSE(tx.hasData()); + EXPECT_FALSE(tx.getData().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/EscrowFinishTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/EscrowFinishTests.cpp index f76bfffb59..e062e24813 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/EscrowFinishTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/EscrowFinishTests.cpp @@ -34,6 +34,7 @@ TEST(TransactionsEscrowFinishTests, BuilderSettersRoundTrip) auto const fulfillmentValue = canonical_VL(); auto const conditionValue = canonical_VL(); auto const credentialIDsValue = canonical_VECTOR256(); + auto const gasValue = canonical_UINT32(); EscrowFinishBuilder builder{ accountValue, @@ -47,6 +48,7 @@ TEST(TransactionsEscrowFinishTests, BuilderSettersRoundTrip) builder.setFulfillment(fulfillmentValue); builder.setCondition(conditionValue); builder.setCredentialIDs(credentialIDsValue); + builder.setGas(gasValue); auto tx = builder.build(publicKey, secretKey); @@ -100,6 +102,14 @@ TEST(TransactionsEscrowFinishTests, BuilderSettersRoundTrip) EXPECT_TRUE(tx.hasCredentialIDs()); } + { + auto const& expected = gasValue; + auto const actualOpt = tx.getGas(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGas should be present"; + expectEqualField(expected, *actualOpt, "sfGas"); + EXPECT_TRUE(tx.hasGas()); + } + } // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, @@ -121,6 +131,7 @@ TEST(TransactionsEscrowFinishTests, BuilderFromStTxRoundTrip) auto const fulfillmentValue = canonical_VL(); auto const conditionValue = canonical_VL(); auto const credentialIDsValue = canonical_VECTOR256(); + auto const gasValue = canonical_UINT32(); // Build an initial transaction EscrowFinishBuilder initialBuilder{ @@ -134,6 +145,7 @@ TEST(TransactionsEscrowFinishTests, BuilderFromStTxRoundTrip) initialBuilder.setFulfillment(fulfillmentValue); initialBuilder.setCondition(conditionValue); initialBuilder.setCredentialIDs(credentialIDsValue); + initialBuilder.setGas(gasValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -185,6 +197,13 @@ TEST(TransactionsEscrowFinishTests, BuilderFromStTxRoundTrip) expectEqualField(expected, *actualOpt, "sfCredentialIDs"); } + { + auto const& expected = gasValue; + auto const actualOpt = rebuiltTx.getGas(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGas should be present"; + expectEqualField(expected, *actualOpt, "sfGas"); + } + } // 3) Verify wrapper throws when constructed from wrong transaction type. @@ -250,6 +269,8 @@ TEST(TransactionsEscrowFinishTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getCondition().has_value()); EXPECT_FALSE(tx.hasCredentialIDs()); EXPECT_FALSE(tx.getCredentialIDs().has_value()); + EXPECT_FALSE(tx.hasGas()); + EXPECT_FALSE(tx.getGas().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/SetFeeTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/SetFeeTests.cpp index 9605daee9f..d7cfb469ba 100644 --- a/src/tests/libxrpl/protocol_autogen/transactions/SetFeeTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/transactions/SetFeeTests.cpp @@ -37,6 +37,9 @@ TEST(TransactionsSetFeeTests, BuilderSettersRoundTrip) auto const baseFeeDropsValue = canonical_AMOUNT(); auto const reserveBaseDropsValue = canonical_AMOUNT(); auto const reserveIncrementDropsValue = canonical_AMOUNT(); + auto const gasLimitValue = canonical_UINT32(); + auto const bytecodeSizeLimitValue = canonical_UINT32(); + auto const gasPriceValue = canonical_UINT32(); SetFeeBuilder builder{ accountValue, @@ -53,6 +56,9 @@ TEST(TransactionsSetFeeTests, BuilderSettersRoundTrip) builder.setBaseFeeDrops(baseFeeDropsValue); builder.setReserveBaseDrops(reserveBaseDropsValue); builder.setReserveIncrementDrops(reserveIncrementDropsValue); + builder.setGasLimit(gasLimitValue); + builder.setBytecodeSizeLimit(bytecodeSizeLimitValue); + builder.setGasPrice(gasPriceValue); auto tx = builder.build(publicKey, secretKey); @@ -134,6 +140,30 @@ TEST(TransactionsSetFeeTests, BuilderSettersRoundTrip) EXPECT_TRUE(tx.hasReserveIncrementDrops()); } + { + auto const& expected = gasLimitValue; + auto const actualOpt = tx.getGasLimit(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasLimit should be present"; + expectEqualField(expected, *actualOpt, "sfGasLimit"); + EXPECT_TRUE(tx.hasGasLimit()); + } + + { + auto const& expected = bytecodeSizeLimitValue; + auto const actualOpt = tx.getBytecodeSizeLimit(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecodeSizeLimit should be present"; + expectEqualField(expected, *actualOpt, "sfBytecodeSizeLimit"); + EXPECT_TRUE(tx.hasBytecodeSizeLimit()); + } + + { + auto const& expected = gasPriceValue; + auto const actualOpt = tx.getGasPrice(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasPrice should be present"; + expectEqualField(expected, *actualOpt, "sfGasPrice"); + EXPECT_TRUE(tx.hasGasPrice()); + } + } // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, @@ -158,6 +188,9 @@ TEST(TransactionsSetFeeTests, BuilderFromStTxRoundTrip) auto const baseFeeDropsValue = canonical_AMOUNT(); auto const reserveBaseDropsValue = canonical_AMOUNT(); auto const reserveIncrementDropsValue = canonical_AMOUNT(); + auto const gasLimitValue = canonical_UINT32(); + auto const bytecodeSizeLimitValue = canonical_UINT32(); + auto const gasPriceValue = canonical_UINT32(); // Build an initial transaction SetFeeBuilder initialBuilder{ @@ -174,6 +207,9 @@ TEST(TransactionsSetFeeTests, BuilderFromStTxRoundTrip) initialBuilder.setBaseFeeDrops(baseFeeDropsValue); initialBuilder.setReserveBaseDrops(reserveBaseDropsValue); initialBuilder.setReserveIncrementDrops(reserveIncrementDropsValue); + initialBuilder.setGasLimit(gasLimitValue); + initialBuilder.setBytecodeSizeLimit(bytecodeSizeLimitValue); + initialBuilder.setGasPrice(gasPriceValue); auto initialTx = initialBuilder.build(publicKey, secretKey); @@ -248,6 +284,27 @@ TEST(TransactionsSetFeeTests, BuilderFromStTxRoundTrip) expectEqualField(expected, *actualOpt, "sfReserveIncrementDrops"); } + { + auto const& expected = gasLimitValue; + auto const actualOpt = rebuiltTx.getGasLimit(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasLimit should be present"; + expectEqualField(expected, *actualOpt, "sfGasLimit"); + } + + { + auto const& expected = bytecodeSizeLimitValue; + auto const actualOpt = rebuiltTx.getBytecodeSizeLimit(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfBytecodeSizeLimit should be present"; + expectEqualField(expected, *actualOpt, "sfBytecodeSizeLimit"); + } + + { + auto const& expected = gasPriceValue; + auto const actualOpt = rebuiltTx.getGasPrice(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfGasPrice should be present"; + expectEqualField(expected, *actualOpt, "sfGasPrice"); + } + } // 3) Verify wrapper throws when constructed from wrong transaction type. @@ -319,6 +376,12 @@ TEST(TransactionsSetFeeTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(tx.getReserveBaseDrops().has_value()); EXPECT_FALSE(tx.hasReserveIncrementDrops()); EXPECT_FALSE(tx.getReserveIncrementDrops().has_value()); + EXPECT_FALSE(tx.hasGasLimit()); + EXPECT_FALSE(tx.getGasLimit().has_value()); + EXPECT_FALSE(tx.hasBytecodeSizeLimit()); + EXPECT_FALSE(tx.getBytecodeSizeLimit().has_value()); + EXPECT_FALSE(tx.hasGasPrice()); + EXPECT_FALSE(tx.getGasPrice().has_value()); } } diff --git a/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp index 4be6e6f1af..283ef7cb3f 100644 --- a/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp +++ b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp @@ -21,6 +21,8 @@ #include #include +#include +#include namespace xrpl { @@ -32,12 +34,13 @@ fillTransaction( std::uint32_t& sequence, ReadView const& ledger) { - txArray["Sequence"] = json::UInt(sequence++); - txArray["Account"] = toBase58(accountID); + txArray[jss::Sequence] = json::UInt(sequence++); + txArray[jss::Account] = toBase58(accountID); auto& fees = ledger.fees(); // Convert the reference transaction cost in fee units to drops // scaled to represent the current fee load. - txArray["Fee"] = scaleFeeLoad(fees.base, context.app.getFeeTrack(), fees, false).jsonClipped(); + txArray[jss::Fee] = + scaleFeeLoad(fees.base, context.app.getFeeTrack(), fees, false).jsonClipped(); } // { @@ -53,24 +56,33 @@ doNoRippleCheck(rpc::JsonContext& context) { auto const& params(context.params); if (!params.isMember(jss::account)) - return rpc::missingFieldError("account"); - - if (!params.isMember("role")) - return rpc::missingFieldError("role"); + return rpc::missingFieldError(jss::account); if (!params[jss::account].isString()) return rpc::invalidFieldError(jss::account); + auto id = parseBase58(params[jss::account].asString()); + if (!id) + { + return rpcError(RpcActMalformed); + } + auto const accountID{id.value()}; + + if (!params.isMember(jss::role)) + return rpc::missingFieldError(jss::role); + bool roleGateway = false; { - std::string const role = params["role"].asString(); - if (role == "gateway") + if (!params[jss::role].isString()) + return rpc::expectedFieldError(jss::role, "string"); + std::string const role = params[jss::role].asString(); + if (role == jss::gateway) { roleGateway = true; } - else if (role != "user") + else if (role != jss::user) { - return rpc::invalidFieldError("role"); + return rpc::invalidFieldError(jss::role); } } @@ -78,61 +90,49 @@ doNoRippleCheck(rpc::JsonContext& context) if (auto err = readLimitField(limit, rpc::tuning::kNoRippleCheck, context)) return *err; - bool transactions = false; - if (params.isMember(jss::transactions)) - transactions = params["transactions"].asBool(); - - // The document[https://xrpl.org/noripple_check.html#noripple_check] states - // that transactions params is a boolean value, however, assigning any - // string value works. Do not allow this. This check is for api Version 2 - // onwards only + // API v1 silently accepts any string as `transactions`; v2+ enforces bool. if (context.apiVersion > 1u && params.isMember(jss::transactions) && !params[jss::transactions].isBool()) { return rpc::invalidFieldError(jss::transactions); } + bool transactions = false; + if (params.isMember(jss::transactions)) + transactions = params[jss::transactions].asBool(); + std::shared_ptr ledger; auto result = rpc::lookupLedger(ledger, context); if (!ledger) return result; - json::Value dummy; // NOLINT(misc-const-correctness) - json::Value& jvTransactions = - transactions ? (result[jss::transactions] = json::ValueType::Array) : dummy; - - auto id = parseBase58(params[jss::account].asString()); - if (!id) - { - rpc::injectError(RpcActMalformed, result); - return result; - } - auto const accountID{id.value()}; auto const sle = ledger->read(keylet::account(accountID)); if (!sle) return rpcError(RpcActNotFound); std::uint32_t seq = sle->getFieldU32(sfSequence); - json::Value& problems = (result["problems"] = json::ValueType::Array); + json::Value& problems = (result[jss::problems] = json::ValueType::Array); - bool const bDefaultRipple = sle->isFlag(lsfDefaultRipple); + bool const defaultRipple = sle->isFlag(lsfDefaultRipple); - if (bDefaultRipple && !roleGateway) + json::Value jvTransactions = json::ValueType::Array; + + if (defaultRipple && !roleGateway) { problems.append( "You appear to have set your default ripple flag even though you " "are not a gateway. This is not recommended unless you are " "experimenting"); } - else if (roleGateway && !bDefaultRipple) + else if (roleGateway && !defaultRipple) { problems.append("You should immediately set your default ripple flag"); if (transactions) { json::Value& tx = jvTransactions.append(json::ValueType::Object); - tx["TransactionType"] = jss::AccountSet; - tx["SetFlag"] = 8; + tx[jss::TransactionType] = jss::AccountSet; + tx[jss::SetFlag] = 8; fillTransaction(context, tx, accountID, seq, *ledger); } } @@ -140,18 +140,18 @@ doNoRippleCheck(rpc::JsonContext& context) forEachItemAfter(*ledger, accountID, uint256(), 0, limit, [&](SLE::const_ref ownedItem) { if (ownedItem->getType() == ltRIPPLE_STATE) { - bool const bLow = accountID == ownedItem->getFieldAmount(sfLowLimit).getIssuer(); + bool const low = accountID == ownedItem->getFieldAmount(sfLowLimit).getIssuer(); - bool const bNoRipple = ownedItem->isFlag(bLow ? lsfLowNoRipple : lsfHighNoRipple); + bool const noRipple = ownedItem->isFlag(low ? lsfLowNoRipple : lsfHighNoRipple); std::string problem; bool needFix = false; - if (bNoRipple && roleGateway) + if (noRipple && roleGateway) { problem = "You should clear the no ripple flag on your "; needFix = true; } - else if (!roleGateway && !bNoRipple) + else if (!roleGateway && !noRipple) { problem = "You should probably set the no ripple flag on your "; needFix = true; @@ -159,22 +159,25 @@ doNoRippleCheck(rpc::JsonContext& context) if (needFix) { AccountID const peer = - ownedItem->getFieldAmount(bLow ? sfHighLimit : sfLowLimit).getIssuer(); + ownedItem->getFieldAmount(low ? sfHighLimit : sfLowLimit).getIssuer(); STAmount const peerLimit = - ownedItem->getFieldAmount(bLow ? sfHighLimit : sfLowLimit); + ownedItem->getFieldAmount(low ? sfHighLimit : sfLowLimit); problem += to_string(peerLimit.get().currency); problem += " line to "; problem += to_string(peerLimit.getIssuer()); problems.append(problem); - STAmount limitAmount(ownedItem->getFieldAmount(bLow ? sfLowLimit : sfHighLimit)); + STAmount limitAmount(ownedItem->getFieldAmount(low ? sfLowLimit : sfHighLimit)); limitAmount.get().account = peer; - json::Value& tx = jvTransactions.append(json::ValueType::Object); - tx["TransactionType"] = jss::TrustSet; - tx["LimitAmount"] = limitAmount.getJson(JsonOptions::Values::None); - tx["Flags"] = bNoRipple ? tfClearNoRipple : tfSetNoRipple; - fillTransaction(context, tx, accountID, seq, *ledger); + if (transactions) + { + json::Value& tx = jvTransactions.append(json::ValueType::Object); + tx[jss::TransactionType] = jss::TrustSet; + tx[jss::LimitAmount] = limitAmount.getJson(JsonOptions::Values::None); + tx[jss::Flags] = noRipple ? tfClearNoRipple : tfSetNoRipple; + fillTransaction(context, tx, accountID, seq, *ledger); + } return true; } @@ -182,6 +185,8 @@ doNoRippleCheck(rpc::JsonContext& context) return false; }); + if (transactions) + result[jss::transactions] = std::move(jvTransactions); return result; }