diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index afb1b0471e..61b27ee1ba 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -116,6 +116,7 @@ test.jtx > xrpl.config test.jtx > xrpl.core test.jtx > xrpld.app test.jtx > xrpld.core +test.jtx > xrpld.overlay test.jtx > xrpld.rpc test.jtx > xrpl.json test.jtx > xrpl.ledger diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8721ad7863..7212214f01 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,11 @@ repos: entry: ./bin/pre-commit/check_doxygen_style.py language: python types_or: [c++, c] + - id: fix-gtest-names + name: "fix gtest names: CamelCase suite, snake_case test case" + entry: ./bin/pre-commit/fix_gtest_names.py + language: python + types_or: [c++, c] - repo: https://github.com/pre-commit/mirrors-clang-format rev: f4d7745e17a28aad7eed2f4874ca8d1568c11c4c # frozen: v22.1.8 diff --git a/OpenTelemetryPlan/00-tracing-fundamentals.md b/OpenTelemetryPlan/00-tracing-fundamentals.md index 1c7675243a..44ff504bcc 100644 --- a/OpenTelemetryPlan/00-tracing-fundamentals.md +++ b/OpenTelemetryPlan/00-tracing-fundamentals.md @@ -209,7 +209,9 @@ flowchart LR subgraph links["Span Links"] direction TB - X["Span X\n(Trace 1)"] -.-|link| Y["Span Y\n(Trace 2)"] + X["`Span X +(Trace 1)`"] -.-|link| Y["`Span Y +(Trace 2)`"] end parent_child ~~~ follows_from ~~~ links @@ -354,9 +356,11 @@ flowchart TB Fn["trace_id = f(ledger_hash)"]:::note --> F1["fetch.request"] --> F2["fetch.receive"] --> F3["fetch.apply"] end - C1 -.-|"span link\n(tx traces)"| T3 + C1 -.-|"`span link +(tx traces)`"| T3 C3 --> V1 - F1 -.-|"span link\n(target ledger)"| C3 + F1 -.-|"`span link +(target ledger)`"| C3 classDef note fill:none,stroke:#888,stroke-dasharray:5 5,color:#333,font-style:italic style T1 fill:#0d47a1,stroke:#082f6a,color:#ffffff diff --git a/bin/pre-commit/fix_gtest_names.py b/bin/pre-commit/fix_gtest_names.py new file mode 100755 index 0000000000..7dbbdf63ba --- /dev/null +++ b/bin/pre-commit/fix_gtest_names.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +""" +Rewrites gtest names to the required style in this project: the suite name is +CamelCase, the test-case name is snake_case. + + TEST(SuiteName, test_case_name) + +The gtest `DISABLED_` prefix is kept verbatim on either name. + +Both conversions fold acronyms the way a reader expects: +`SetAndResetAccountTxnID` -> `set_and_reset_account_txn_id`, not +`set_and_reset_account_txn_i_d`. + +The first argument of `TEST_F`, `TEST_P`, `TYPED_TEST` and `TYPED_TEST_P` is a +fixture class rather than a free identifier, so rewriting it here would leave +the class it names behind. Those are reported for a human to rename (clang-tidy +checks the class declaration itself, via readability-identifier-naming). + +Usage: ./bin/pre-commit/fix_gtest_names.py ... +""" + +import re +import sys +from collections import Counter +from pathlib import Path + +# A test-case definition, `MACRO(SuiteOrFixture, TestName)`, anchored at the +# start of a line so that commented-out definitions and project macros that +# merely look similar (`TEST_EXPECT(...)`) are left alone. The `\s*` between +# arguments allows for a definition clang-format wrapped over several lines. +PATTERN = re.compile( + r"(?P^[ \t]*(?PTYPED_TEST_P|TYPED_TEST|TEST_F|TEST_P|TEST)\s*\(\s*)" + r"(?P\w+)(?P\s*,\s*)(?P\w+)(?P\s*\))", + re.MULTILINE, +) + +# The macros whose first argument names a fixture class, not a free identifier. +FIXTURE_MACROS = ("TEST_F", "TEST_P", "TYPED_TEST", "TYPED_TEST_P") + +DISABLED = "DISABLED_" + +ACRONYM_BOUNDARY = re.compile(r"([A-Z]+)([A-Z][a-z])") +WORD_BOUNDARY = re.compile(r"([a-z\d])([A-Z])") + + +def _split_disabled(name: str) -> tuple[str, str]: + """Splits off gtest's `DISABLED_` prefix, which is kept verbatim.""" + if name.startswith(DISABLED): + return DISABLED, name[len(DISABLED) :] + return "", name + + +def snake_case(name: str) -> str: + """Returns the name in snake_case, leaving acronyms whole. + + `SetAndResetAccountTxnID` -> `set_and_reset_account_txn_id`, + `parseStatRSSkB` -> `parse_stat_rs_sk_b`. + """ + prefix, core = _split_disabled(name) + core = ACRONYM_BOUNDARY.sub(r"\1_\2", core) + return prefix + WORD_BOUNDARY.sub(r"\1_\2", core).lower() + + +def camel_case(name: str) -> str: + """Returns the name in CamelCase, capitalizing each underscored word. + + Only the letters that have to change are touched, so acronyms survive: a + conversion that went via snake_case would turn `SHAMapTest` into + `ShaMapTest`, whereas here it is already CamelCase and stays put. + `json_value` -> `JsonValue`, `parseStatRSSkB` -> `ParseStatRSSkB`. + """ + prefix, core = _split_disabled(name) + return prefix + "".join(w[:1].upper() + w[1:] for w in core.split("_") if w) + + +def _corrected(match: re.Match) -> tuple[str, str]: + """Returns the suite and test-case names this definition should end up with.""" + suite = match["suite"] + return ( + suite if match["macro"] in FIXTURE_MACROS else camel_case(suite), + snake_case(match["name"]), + ) + + +def fix_source(text: str) -> tuple[str, list[str]]: + """Returns the corrected text and one `line: message` report per bad name.""" + # gtest joins the suite and test names into one class name, so two test + # cases whose joined names agree cannot coexist: `TEST(a, b_c)` and + # `TEST(a_b, c)` both define `a_b_c_Test`. A rename that would introduce + # such a clash is reported for a human instead of applied. + joined = Counter("_".join(_corrected(m)) for m in PATTERN.finditer(text)) + reports = [] + + def rewrite(match: re.Match) -> str: + suite, name = match["suite"], match["name"] + new_suite, new_name = _corrected(match) + line = text.count("\n", 0, match.start()) + 1 + + if match["macro"] in FIXTURE_MACROS and camel_case(suite) != suite: + reports.append( + f"{line}: fixture '{suite}' is not CamelCase: rename the class " + f"to '{camel_case(suite)}' by hand" + ) + if (new_suite, new_name) == (suite, name): + return match[0] + if joined[f"{new_suite}_{new_name}"] > 1: + reports.append( + f"{line}: cannot rename '{suite}, {name}' to '{new_suite}, " + f"{new_name}': another test case already generates that name" + ) + return match[0] + if new_suite != suite: + reports.append(f"{line}: renamed suite '{suite}' to '{new_suite}'") + if new_name != name: + reports.append(f"{line}: renamed test case '{name}' to '{new_name}'") + return match["head"] + new_suite + match["mid"] + new_name + match["tail"] + + return PATTERN.sub(rewrite, text), reports + + +def fix_names(path: Path) -> bool: + """Corrects one file's gtest names, reporting each on stdout.""" + original = path.read_text(encoding="utf-8") + fixed, reports = fix_source(original) + for report in reports: + print(f"{path}:{report}") + if fixed != original: + path.write_text(fixed, encoding="utf-8") + return not reports + + +def main() -> int: + files = [Path(f) for f in sys.argv[1:]] + success = True + + for path in files: + success &= fix_names(path) + + return 0 if success else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/bin/pre-commit/test_fix_gtest_names.py b/bin/pre-commit/test_fix_gtest_names.py new file mode 100755 index 0000000000..aa0481f795 --- /dev/null +++ b/bin/pre-commit/test_fix_gtest_names.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +""" +Tests for fix_gtest_names.py. + +Run directly (no test framework needed): + ./bin/pre-commit/test_fix_gtest_names.py +or under pytest: + pytest bin/pre-commit/test_fix_gtest_names.py +""" + +import sys +import textwrap + +from fix_gtest_names import camel_case, fix_source, snake_case + + +def dedent(text: str) -> str: + """Removes a fixture's common indentation and its leading newline. + + Lets fixtures be written as indented triple-quoted here-docs while keeping + honest 1-based line numbers. + """ + return textwrap.dedent(text).lstrip("\n") + + +def fixed(text: str) -> str: + return fix_source(dedent(text))[0] + + +def reports(text: str) -> list[str]: + return fix_source(dedent(text))[1] + + +# --- conversion -------------------------------------------------------------- + + +def test_snake_case_conversion() -> None: + assert snake_case("BadInputs") == "bad_inputs" + assert snake_case("mulDiv") == "mul_div" + assert snake_case("already_snake") == "already_snake" + assert snake_case("base64") == "base64" + + +def test_snake_case_keeps_acronyms_whole() -> None: + assert snake_case("SetAndResetAccountTxnID") == "set_and_reset_account_txn_id" + assert snake_case("XRPToIOU") == "xrp_to_iou" + assert snake_case("STAmountMath") == "st_amount_math" + + +def test_camel_case_conversion() -> None: + assert camel_case("json_value") == "JsonValue" + assert camel_case("mulDiv") == "MulDiv" + assert camel_case("scope") == "Scope" + assert camel_case("base64") == "Base64" + + +def test_camel_case_leaves_acronyms_alone() -> None: + # A snake_case round-trip would give `ShaMapTest` / `ParseStatmRsSkB` here. + assert camel_case("SHAMapTest") == "SHAMapTest" + assert camel_case("parseStatmRSSkB") == "ParseStatmRSSkB" + assert camel_case("XRPAmount") == "XRPAmount" + assert camel_case("CSPRNG") == "CSPRNG" + + +def test_disabled_prefix_preserved() -> None: + assert snake_case("DISABLED_FooBar") == "DISABLED_foo_bar" + assert snake_case("DISABLED_foo_bar") == "DISABLED_foo_bar" + assert snake_case("DISABLED_") == "DISABLED_" + assert camel_case("DISABLED_foo_bar") == "DISABLED_FooBar" + assert camel_case("DISABLED_") == "DISABLED_" + + +# --- what counts as a test definition --------------------------------------- + + +def test_all_macros_recognized() -> None: + code = """ + TEST(Suite, oneName) + TEST_F(Fixture, twoName) + TEST_P(Fixture, threeName) + TYPED_TEST(Fixture, fourName) + TYPED_TEST_P(Fixture, fiveName) + """ + assert fixed(code) == dedent(""" + TEST(Suite, one_name) + TEST_F(Fixture, two_name) + TEST_P(Fixture, three_name) + TYPED_TEST(Fixture, four_name) + TYPED_TEST_P(Fixture, five_name) + """) + + +def test_conforming_definitions_untouched() -> None: + code = """ + TEST(AccountSet, bad_inputs) + TEST_F(MutexMakeTest, default_constructor) + TEST(SHAMap, DISABLED_slow_path) + """ + assert reports(code) == [] + assert fixed(code) == dedent(code) + + +def test_lookalikes_ignored() -> None: + code = """ + // TEST(Suite, notATest) + TEST_EXPECT(someCall()) + TEST_EXPECTS(amount == value, amount.getText()) + INSTANTIATE_TEST_SUITE_P(Prefix, Fixture, testValues()); + auto x = TEST(Suite, notATest); + TYPED_TEST_SUITE(Fixture, MyTypes); + """ + assert reports(code) == [] + assert fixed(code) == dedent(code) + + +def test_indented_and_wrapped_definitions() -> None: + code = """ + namespace ripple { + TEST(Suite, indentedName) + } + TEST_F( + SomeVeryLongFixtureName, + wrappedName) + """ + assert fixed(code) == dedent(""" + namespace ripple { + TEST(Suite, indented_name) + } + TEST_F( + SomeVeryLongFixtureName, + wrapped_name) + """) + + +# --- rewriting -------------------------------------------------------------- + + +def test_only_the_two_names_are_rewritten() -> None: + code = """ + TEST(mulDiv, mulDiv) + { + auto const mulDiv = 1; // mulDiv stays + } + """ + assert fixed(code) == dedent(""" + TEST(MulDiv, mul_div) + { + auto const mulDiv = 1; // mulDiv stays + } + """) + + +def test_suite_name_camel_cased() -> None: + code = """ + TEST(json_value, limits) + TEST(scope, ScopeExit) + """ + assert reports(code) == [ + "1: renamed suite 'json_value' to 'JsonValue'", + "2: renamed suite 'scope' to 'Scope'", + "2: renamed test case 'ScopeExit' to 'scope_exit'", + ] + assert fixed(code) == dedent(""" + TEST(JsonValue, limits) + TEST(Scope, scope_exit) + """) + + +def test_fixture_reported_but_not_renamed() -> None: + # The first argument names a class, so only a human (or clang-tidy) can + # rename it; the test-case name is still fixed. + code = """ + TEST_F(my_fixture, someTest) + """ + assert reports(code) == [ + "1: fixture 'my_fixture' is not CamelCase: rename the class to " + "'MyFixture' by hand", + "1: renamed test case 'someTest' to 'some_test'", + ] + assert fixed(code) == dedent(""" + TEST_F(my_fixture, some_test) + """) + + +def test_reports_carry_line_numbers() -> None: + code = """ + #include + + TEST(Suite, firstName) + + TEST(Suite, secondName) + """ + assert reports(code) == [ + "3: renamed test case 'firstName' to 'first_name'", + "5: renamed test case 'secondName' to 'second_name'", + ] + + +# --- collisions ------------------------------------------------------------- + + +def test_collision_reported_and_not_applied() -> None: + # Both would define `Suite_mul_div_Test`. + code = """ + TEST(Suite, mulDiv) + TEST(Suite, mul_div) + """ + assert reports(code) == [ + "1: cannot rename 'Suite, mulDiv' to 'Suite, mul_div': another test " + "case already generates that name" + ] + assert fixed(code) == dedent(code) + + +def test_collision_between_converging_suites() -> None: + # Both suites camel-case to `SuiteA`, so both would define + # `SuiteA_one_test_Test`. + code = """ + TEST(SuiteA, oneTest) + TEST(Suite_a, one_test) + """ + assert [r.split(":")[1].strip() for r in reports(code)] == [ + "cannot rename 'SuiteA, oneTest' to 'SuiteA, one_test'", + "cannot rename 'Suite_a, one_test' to 'SuiteA, one_test'", + ] + assert fixed(code) == dedent(code) + + +def test_same_name_in_different_suites_is_not_a_collision() -> None: + code = """ + TEST(SuiteOne, mulDiv) + TEST(SuiteTwo, mulDiv) + """ + assert fixed(code) == dedent(""" + TEST(SuiteOne, mul_div) + TEST(SuiteTwo, mul_div) + """) + + +def main() -> int: + tests = sorted( + (name, fn) + for name, fn in globals().items() + if name.startswith("test_") and callable(fn) + ) + failed = 0 + for name, fn in tests: + try: + fn() + print(f"PASS {name}") + except AssertionError as exc: + failed += 1 + print(f"FAIL {name}: {exc!r}") + print(f"\n{len(tests) - failed}/{len(tests)} passed") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/conanfile.py b/conanfile.py index 1fd1ff842e..5a8c2318d0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -239,3 +239,7 @@ class Xrpl(ConanFile): libxrpl.requires.append("rocksdb::librocksdb") if self.options.telemetry: libxrpl.requires.append("opentelemetry-cpp::opentelemetry-cpp") + # The public telemetry headers pick their class layout on this + # define, so a consumer that does not see it compiles a different + # SpanGuard than the one inside the library it links. + libxrpl.defines.append("XRPL_ENABLE_TELEMETRY") diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index 3706ef94e3..5c52fb0ba3 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -347,6 +347,30 @@ isAuditorMirrorCurrent(SLE const& issuance, SLE const& mptoken); [[nodiscard]] bool areMirrorsCurrent(SLE const& issuance, SLE const& mptoken); +/** + * @brief Set the holder's issuer mirror epoch to match the issuance's current issuer key epoch. + * + * Call this after writing the issuer mirror ciphertext under the issuance's + * currently registered issuer key, so that the mirror reads as current afterwards. + * + * @param issuance The MPTokenIssuance ledger object. + * @param mptoken The holder's MPToken ledger entry to update. + */ +void +setIssuerMirrorEpoch(SLE const& issuance, SLE& mptoken); + +/** + * @brief Set the holder's auditor mirror epoch to match the issuance's current auditor key epoch. + * + * Call this after writing the auditor mirror ciphertext under the issuance's + * currently registered auditor key. Does nothing when the holder has no auditor mirror. + * + * @param issuance The MPTokenIssuance ledger object. + * @param mptoken The holder's MPToken ledger entry to update. + */ +void +setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken); + /** * @brief Set the holder's MPToken mirror epochs to match the issuance's current key epochs. * diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 0170cbb88a..61f246c752 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -540,6 +540,11 @@ constexpr std::size_t kEcConvertBackProofLength = */ constexpr std::size_t kEcClawbackProofLength = SECP256K1_COMPACT_CLAWBACK_PROOF_SIZE; +/** + * Length of compact equality proof. + */ +constexpr std::size_t kEcEqualityProofLength = 128; + /** * Extra base fee multiplier charged to confidential MPT transactions. */ diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 454aa85ffd..cb3d5fd2b1 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1134,6 +1134,19 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, {sfRemainingOwnerCountDelta, SoeOptional}, })) +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, 92, ConfidentialMPTMirrorUpdate, + ({.delegable = Delegation::Delegable, .amendment = featureConfidentialMPTKeyRotation}), + ({ + {sfMPTokenIssuanceID, SoeRequired}, + {sfHolder, SoeOptional}, + {sfIssuerEncryptedAmount, SoeOptional}, + {sfAuditorEncryptedAmount, SoeOptional}, + {sfZKProof, SoeRequired}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMirrorUpdate.h b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMirrorUpdate.h new file mode 100644 index 0000000000..1fc25bca99 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/ConfidentialMPTMirrorUpdate.h @@ -0,0 +1,266 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class ConfidentialMPTMirrorUpdateBuilder; + +/** + * @brief Transaction: ConfidentialMPTMirrorUpdate + * + * Type: ttCONFIDENTIAL_MPT_MIRROR_UPDATE (92) + * Delegable: Delegation::Delegable + * Amendment: featureConfidentialMPTKeyRotation + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use ConfidentialMPTMirrorUpdateBuilder to construct new transactions. + */ +class ConfidentialMPTMirrorUpdate : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttCONFIDENTIAL_MPT_MIRROR_UPDATE; + + /** + * @brief Construct a ConfidentialMPTMirrorUpdate transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit ConfidentialMPTMirrorUpdate(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTMirrorUpdate"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT192::type::value_type + getMPTokenIssuanceID() const + { + return this->tx_->at(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfHolder (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getHolder() const + { + if (hasHolder()) + { + return this->tx_->at(sfHolder); + } + return std::nullopt; + } + + /** + * @brief Check if sfHolder is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasHolder() const + { + return this->tx_->isFieldPresent(sfHolder); + } + + /** + * @brief Get sfIssuerEncryptedAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getIssuerEncryptedAmount() const + { + if (hasIssuerEncryptedAmount()) + { + return this->tx_->at(sfIssuerEncryptedAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfIssuerEncryptedAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasIssuerEncryptedAmount() const + { + return this->tx_->isFieldPresent(sfIssuerEncryptedAmount); + } + + /** + * @brief Get sfAuditorEncryptedAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorEncryptedAmount() const + { + if (hasAuditorEncryptedAmount()) + { + return this->tx_->at(sfAuditorEncryptedAmount); + } + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptedAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptedAmount() const + { + return this->tx_->isFieldPresent(sfAuditorEncryptedAmount); + } + + /** + * @brief Get sfZKProof (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getZKProof() const + { + return this->tx_->at(sfZKProof); + } +}; + +/** + * @brief Builder for ConfidentialMPTMirrorUpdate transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class ConfidentialMPTMirrorUpdateBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new ConfidentialMPTMirrorUpdateBuilder with required fields. + * @param account The account initiating the transaction. + * @param mPTokenIssuanceID The sfMPTokenIssuanceID field value. + * @param zKProof The sfZKProof field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + ConfidentialMPTMirrorUpdateBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& mPTokenIssuanceID, std::decay_t const& zKProof, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttCONFIDENTIAL_MPT_MIRROR_UPDATE, account, sequence, fee) + { + setMPTokenIssuanceID(mPTokenIssuanceID); + setZKProof(zKProof); + } + + /** + * @brief Construct a ConfidentialMPTMirrorUpdateBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + ConfidentialMPTMirrorUpdateBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttCONFIDENTIAL_MPT_MIRROR_UPDATE) + { + throw std::runtime_error("Invalid transaction type for ConfidentialMPTMirrorUpdateBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTMirrorUpdateBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfHolder (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTMirrorUpdateBuilder& + setHolder(std::decay_t const& value) + { + object_[sfHolder] = value; + return *this; + } + + /** + * @brief Set sfIssuerEncryptedAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTMirrorUpdateBuilder& + setIssuerEncryptedAmount(std::decay_t const& value) + { + object_[sfIssuerEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfAuditorEncryptedAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTMirrorUpdateBuilder& + setAuditorEncryptedAmount(std::decay_t const& value) + { + object_[sfAuditorEncryptedAmount] = value; + return *this; + } + + /** + * @brief Set sfZKProof (SoeRequired) + * @return Reference to this builder for method chaining. + */ + ConfidentialMPTMirrorUpdateBuilder& + setZKProof(std::decay_t const& value) + { + object_[sfZKProof] = value; + return *this; + } + + /** + * @brief Build and return the ConfidentialMPTMirrorUpdate wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + ConfidentialMPTMirrorUpdate + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return ConfidentialMPTMirrorUpdate{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/telemetry/SpanGuard.h b/include/xrpl/telemetry/SpanGuard.h index 8ad88829bb..ee37cee4f8 100644 --- a/include/xrpl/telemetry/SpanGuard.h +++ b/include/xrpl/telemetry/SpanGuard.h @@ -447,7 +447,14 @@ public: setAttribute(std::string_view key, std::string_view value) noexcept; /** - * Set a string attribute (C-string overload). No-op on a null guard. + * Set a string attribute from a C string. No-op on a null guard. + * + * @param key Attribute key. + * @param value Null-terminated text. A null pointer records nothing, since + * an empty value is already a meaningful value here. + * @note This overload is required, not a convenience. Without it a string + * literal binds to the bool overload, because pointer-to-bool is a standard + * conversion and beats the std::string_view one. */ void setAttribute(std::string_view key, char const* value) noexcept; @@ -739,7 +746,14 @@ public: setAttribute(std::string_view key, std::string_view value) noexcept; /** - * Set a string attribute (C-string overload). No-op on a null guard. + * Set a string attribute from a C string. No-op on a null guard. + * + * @param key Attribute key. + * @param value Null-terminated text. A null pointer records nothing, since + * an empty value is already a meaningful value here. + * @note This overload is required, not a convenience. Without it a string + * literal binds to the bool overload, because pointer-to-bool is a standard + * conversion and beats the std::string_view one. */ void setAttribute(std::string_view key, char const* value) noexcept; diff --git a/include/xrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.h b/include/xrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.h new file mode 100644 index 0000000000..12ad5c8f28 --- /dev/null +++ b/include/xrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +/** + * @brief Updates the encrypted mirror balances of a Confidential MPToken. + * + * @details + * This transaction updates a single holder's mirrored confidential balances + * (`sfIssuerEncryptedBalance` and/or `sfAuditorEncryptedBalance`) with the latest + * ElGamal public keys defined on the `MPTokenIssuance`. + * + * It supports both issuer and holder self-migration modes, each mode supports multiple flows: + * - Issuer mode: Submitted by the issuer. + * 1. Issuer Key Rotation Migration: Re-encrypts the + * holder's `sfIssuerEncryptedBalance` under the issuer's new ElGamal public key. + * + * 2. Auditor Key Rotation Migration: Re-encrypts the + * holder's `sfAuditorEncryptedBalance` under the auditor's new ElGamal public key. + * + * 3. Simultaneous Rotation Migration: Updates both the issuer + * and auditor encrypted balances in a single transaction to optimize network throughput. + * + * 4. Auditor Late-Registration Migration: When the issuer ElGamal + * public key is already registered on the `MPTokenIssuance` object, the issuer can + * register an auditor key at a later time through `MPTokenIssuanceSet`. Then the issuer uses this + * flow to set the holder's initial `sfAuditorEncryptedBalance` on `MPToken` object. + * + * - Holder self-migration mode: Submitted by the holder. The holder decrypts their own + * `sfConfidentialBalanceSpending` with holder's private key to recover the balance and + * re-encrypts it under the relevant new ElGamal public key(s). This mode is always + * available to the holder and is not conditioned on the issuer being unable to migrate + * them: the ledger cannot verify whether an issuer has really lost its private key. That + * loss is only the expected motivation, since an issuer that still holds its key can + * migrate holders itself in issuer mode. + * @note All holder migration flows strictly require the holder's + * `sfConfidentialBalanceInbox` to be canonically zero; the holder must run + * `ConfidentialMPTMergeInbox` first so the spending balance reflects the + * full balance. + * + * 5. Holder Issuer-Mirror Migration: Re-encrypts the holder's + * `sfIssuerEncryptedBalance` under the issuer's new ElGamal public key. + * + * 6. Holder Auditor-Mirror Migration: Re-encrypts the holder's + * `sfAuditorEncryptedBalance` under the auditor's new ElGamal public key, or + * sets it for the first time when the auditor key was late-registered. This is the + * holder-driven counterpart to flows 2 and 4, for when the issuer does not migrate + * the holder itself. + * + * 7. Simultaneous Holder Self-Migration: Updates both the issuer and auditor + * encrypted balances in a single transaction (both keys have rotated). + */ +class ConfidentialMPTMirrorUpdate : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit ConfidentialMPTMirrorUpdate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry(bool isDelete, SLE::const_ref before, SLE::const_ref after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index 99ae066475..a3e48b5f31 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -435,21 +435,41 @@ areMirrorsCurrent(SLE const& issuance, SLE const& mptoken) } void -setMirrorEpochs(SLE const& issuance, SLE& mptoken) +setIssuerMirrorEpoch(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"); + "xrpl::setIssuerMirrorEpoch : issuance MPTokenIssuance object"); + XRPL_ASSERT( + mptoken.getType() == ltMPTOKEN, "xrpl::setIssuerMirrorEpoch : mptoken MPToken object"); + // Unlike the auditor mirror, the issuer mirror is not optional: every + // confidential MPToken carries one, so there is no existence check here. 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; - } +void +setAuditorMirrorEpoch(SLE const& issuance, SLE& mptoken) +{ + XRPL_ASSERT( + issuance.getType() == ltMPTOKEN_ISSUANCE, + "xrpl::setAuditorMirrorEpoch : issuance MPTokenIssuance object"); + XRPL_ASSERT( + mptoken.getType() == ltMPTOKEN, "xrpl::setAuditorMirrorEpoch : mptoken MPToken object"); + + if (!mptoken.isFieldPresent(sfAuditorEncryptedBalance)) + return; + + if (auto const epoch = issuance[~sfAuditorKeyEpoch].value_or(0); epoch != 0) + mptoken[sfAuditorKeyMirrorEpoch] = epoch; +} + +void +setMirrorEpochs(SLE const& issuance, SLE& mptoken) +{ + setIssuerMirrorEpoch(issuance, mptoken); + setAuditorMirrorEpoch(issuance, mptoken); } TER diff --git a/src/libxrpl/telemetry/NullTelemetry.cpp b/src/libxrpl/telemetry/NullTelemetry.cpp index 81ad9e108a..7f65e980f9 100644 --- a/src/libxrpl/telemetry/NullTelemetry.cpp +++ b/src/libxrpl/telemetry/NullTelemetry.cpp @@ -64,7 +64,13 @@ public: void stop() override { - Telemetry::setInstance(nullptr); + // Clear the global instance only if this object is the one that + // published it. A process with two of these, as the test binary has, + // would otherwise let one unregister the other. + if (Telemetry::getInstance() == this) + { + Telemetry::setInstance(nullptr); + } } [[nodiscard]] bool diff --git a/src/libxrpl/telemetry/SpanGuard.cpp b/src/libxrpl/telemetry/SpanGuard.cpp index 69efdaf29e..439fca96a3 100644 --- a/src/libxrpl/telemetry/SpanGuard.cpp +++ b/src/libxrpl/telemetry/SpanGuard.cpp @@ -412,7 +412,11 @@ SpanGuard::setAttribute(std::string_view key, std::string_view value) noexcept void SpanGuard::setAttribute(std::string_view key, char const* value) noexcept { - setAttribute(key, std::string_view(value)); + // A std::string_view built from a pointer reads that pointer to find its + // length, so a null one is undefined behaviour. A null pointer carries no + // text, and an empty value already means something here, so record nothing. + if (value != nullptr) + setAttribute(key, std::string_view(value)); } void diff --git a/src/libxrpl/telemetry/Telemetry.cpp b/src/libxrpl/telemetry/Telemetry.cpp index a83388d339..475b7fa73b 100644 --- a/src/libxrpl/telemetry/Telemetry.cpp +++ b/src/libxrpl/telemetry/Telemetry.cpp @@ -177,7 +177,13 @@ public: void stop() override { - Telemetry::setInstance(nullptr); + // Clear the global instance only if this object is the one that + // published it. A process with two of these, as the test binary has, + // would otherwise let one unregister the other. + if (Telemetry::getInstance() == this) + { + Telemetry::setInstance(nullptr); + } } [[nodiscard]] bool @@ -371,8 +377,12 @@ public: { JLOG(journal_.info()) << "Telemetry stopping"; - // Unregister global instance before tearing down the pipeline. - Telemetry::setInstance(nullptr); + // Unregister global instance before tearing down the pipeline, but only + // if this object is the one that published it. + if (Telemetry::getInstance() == this) + { + Telemetry::setInstance(nullptr); + } if (sdkProvider_) { diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp b/src/libxrpl/tx/invariants/MPTInvariant.cpp index 46d1037acf..060673c6e1 100644 --- a/src/libxrpl/tx/invariants/MPTInvariant.cpp +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp @@ -40,6 +40,7 @@ constexpr auto kConfidentialMptTxTypes = std::to_array({ ttCONFIDENTIAL_MPT_CONVERT_BACK, ttCONFIDENTIAL_MPT_MERGE_INBOX, ttCONFIDENTIAL_MPT_CLAWBACK, + ttCONFIDENTIAL_MPT_MIRROR_UPDATE, }); // Clamp to the cap (== INT64_MAX) before the signed conversion. Invariant diff --git a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp index c3131714f8..642a415be7 100644 --- a/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp +++ b/src/libxrpl/tx/transactors/sponsor/SponsorshipTransfer.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -206,6 +207,13 @@ SponsorshipTransfer::preflight(PreflightContext const& ctx) return temMALFORMED; } + if (auto const objectID = ctx.tx[~sfObjectID]; + ctx.rules.enabled(fixCleanup3_5_0) && objectID && *objectID == beast::kZero) + { + JLOG(ctx.j.debug()) << "preflight: sfObjectID must not be zero"; + return temMALFORMED; + } + return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.cpp b/src/libxrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.cpp new file mode 100644 index 0000000000..c00486e5e6 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/ConfidentialMPTMirrorUpdate.cpp @@ -0,0 +1,273 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +bool +ConfidentialMPTMirrorUpdate::checkExtraFeatures(PreflightContext const& ctx) +{ + // Key rotation makes sense only when featureConfidentialTransfer is enabled. + return ctx.rules.enabled(featureConfidentialTransfer); +} + +NotTEC +ConfidentialMPTMirrorUpdate::preflight(PreflightContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + auto const issuer = MPTIssue(ctx.tx[sfMPTokenIssuanceID]).getIssuer(); + auto const holder = ctx.tx[~sfHolder]; + bool const hasHolder = holder.has_value(); + + // The rotation mode is determined by the presence of the + // Holder field: Holder present is issuer mode, Holder absent is + // holder self-migration. + if (hasHolder) + { + // Issuer mode: account must be the issuer + if (account != issuer) + return temMALFORMED; + + if (account == *holder) + return temMALFORMED; + } + else + { + // Holder self-migration: the submitter is the holder, account must not be the issuer. + if (account == issuer) + return temMALFORMED; + } + + // At least one ciphertext will be updated. + bool const hasIssuerAmount = ctx.tx.isFieldPresent(sfIssuerEncryptedAmount); + bool const hasAuditorAmount = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + if (!hasIssuerAmount && !hasAuditorAmount) + return temMALFORMED; + + // Check the length of the encrypted amounts. Length check is cheaper than format check so put + // it before the format check. + if (hasIssuerAmount && ctx.tx[sfIssuerEncryptedAmount].length() != kEcGamalEncryptedTotalLength) + return temBAD_CIPHERTEXT; + + if (hasAuditorAmount && + ctx.tx[sfAuditorEncryptedAmount].length() != kEcGamalEncryptedTotalLength) + return temBAD_CIPHERTEXT; + + // Check proof length. + if (ctx.tx[sfZKProof].length() != kEcEqualityProofLength) + return temMALFORMED; + + // Check the encrypted amount formats. It is more expensive so put it at the end of preflight. + if (hasIssuerAmount && !isValidCiphertext(ctx.tx[sfIssuerEncryptedAmount])) + return temBAD_CIPHERTEXT; + + if (hasAuditorAmount && !isValidCiphertext(ctx.tx[sfAuditorEncryptedAmount])) + return temBAD_CIPHERTEXT; + + return tesSUCCESS; +} + +XRPAmount +ConfidentialMPTMirrorUpdate::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier); +} + +TER +ConfidentialMPTMirrorUpdate::preclaim(PreclaimContext const& ctx) +{ + // Check if account exists + auto const account = ctx.tx[sfAccount]; + if (!ctx.view.exists(keylet::account(account))) + return terNO_ACCOUNT; // LCOV_EXCL_LINE + + // The issuance must exist and have confidential balances enabled with a + // registered issuer encryption key; otherwise there is no mirror to update. + auto const mptIssuanceID = ctx.tx[sfMPTokenIssuanceID]; + auto const sleIssuance = ctx.view.read(keylet::mptokenIssuance(mptIssuanceID)); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // The issuance must have confidential balances enabled with a registered issuer encryption key. + if (!sleIssuance->isFlag(lsfMPTCanHoldConfidentialBalance) || + !sleIssuance->isFieldPresent(sfIssuerEncryptionKey)) + return tecNO_PERMISSION; + + // Sanity check: preflight already enforced the issuer holder combination + // under different rotation modes. + auto const holder = ctx.tx[~sfHolder]; + bool const hasHolder = holder.has_value(); + auto const issuer = sleIssuance->getAccountID(sfIssuer); + if (hasHolder ? (issuer != account) : (issuer == account)) + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMirrorUpdate::preclaim : invalid issuer holder combination"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + // The holder is sfHolder in issuer mode and is sfAccount in holder mode. + auto const holderID = hasHolder ? *holder : account; + + // In issuer mode, the holder must exist. In holder mode, the account existence was checked + // already. + if (hasHolder && !ctx.view.exists(keylet::account(holderID))) + return tecNO_TARGET; + + // In either issuer or holder mode, check the existence of the MPToken object. + auto const sleMptoken = ctx.view.read(keylet::mptoken(mptIssuanceID, holderID)); + if (!sleMptoken) + return tecOBJECT_NOT_FOUND; + + // The holder must already hold an issuer confidential balance. + if (!sleMptoken->isFieldPresent(sfIssuerEncryptedBalance)) + return tecNO_PERMISSION; + + bool const hasIssuerAmount = ctx.tx.isFieldPresent(sfIssuerEncryptedAmount); + bool const hasAuditorAmount = ctx.tx.isFieldPresent(sfAuditorEncryptedAmount); + + // Migrating the auditor mirror requires the issuance to have a registered + // auditor encryption key. + if (hasAuditorAmount && !sleIssuance->isFieldPresent(sfAuditorEncryptionKey)) + return tecNO_PERMISSION; + + // An issuer mirror may only be re-encrypted while it is stale, reject if it is already current. + if (hasIssuerAmount && isIssuerMirrorCurrent(*sleIssuance, *sleMptoken)) + return tecNO_PERMISSION; + + if (hasAuditorAmount) + { + // An issuer-mode auditor-only migration: the issuer mirror must already be up to date. + if (hasHolder && !hasIssuerAmount && !isIssuerMirrorCurrent(*sleIssuance, *sleMptoken)) + return tecNO_PERMISSION; + + // An auditor mirror may only be re-encrypted while it is stale, reject if it is already + // current. isAuditorMirrorCurrent reports an absent auditor mirror as stale, which is what + // allows an auditor-only migration to create one for the first time. + if (isAuditorMirrorCurrent(*sleIssuance, *sleMptoken)) + return tecNO_PERMISSION; + } + + // Holder self-migration re-encrypts the mirror from the holder's own + // spending balance, which reflects the holder's full balance only once the + // inbox has been merged into it. Require the inbox to be canonical zero, + // i.e. ConfidentialMPTMergeInbox has already been applied. + if (!hasHolder) + { + // Sanity check: a holder that already carries an issuer mirror + // necessarily has a holder encryption key and a spending balance + if (!sleMptoken->isFieldPresent(sfHolderEncryptionKey) || + !sleMptoken->isFieldPresent(sfConfidentialBalanceSpending)) + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMirrorUpdate::preclaim : an issuer mirror implies a holder " + "key and spending balance"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + auto const expectedZeroInbox = encryptCanonicalZeroAmount( + (*sleMptoken)[sfHolderEncryptionKey], holderID, mptIssuanceID); + if (!expectedZeroInbox) + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMirrorUpdate::preclaim : canonical zero encryption cannot " + "fail for an already-valid holder public key"); + return tefINTERNAL; + // LCOV_EXCL_STOP + } + + bool const inboxIsCanonicalZero = sleMptoken->isFieldPresent(sfConfidentialBalanceInbox) && + Slice((*sleMptoken)[sfConfidentialBalanceInbox]) == Slice(*expectedZeroInbox); + if (!inboxIsCanonicalZero) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +ConfidentialMPTMirrorUpdate::doApply() +{ + auto const mptIssuanceID = ctx_.tx[sfMPTokenIssuanceID]; + + auto const sleIssuance = view().read(keylet::mptokenIssuance(mptIssuanceID)); + if (!sleIssuance) + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMirrorUpdate::doApply : preclaim already validated the " + "issuance exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + // The holderID is sfHolder in issuer mode and sfAccount in holder mode. + auto const holder = ctx_.tx[~sfHolder]; + auto const holderID = holder.value_or(accountID_); + + auto sleMptoken = view().peek(keylet::mptoken(mptIssuanceID, holderID)); + if (!sleMptoken) + { + // LCOV_EXCL_START + UNREACHABLE( + "xrpl::ConfidentialMPTMirrorUpdate::doApply : preclaim already validated the " + "MPToken exists"); + return tecINTERNAL; + // LCOV_EXCL_STOP + } + + // Re-encrypt the requested mirror(s) and advance the corresponding mirror + // epoch to match the issuance key epoch. Each mirror is stamped separately + // because this transaction may migrate either one or both. + if (ctx_.tx.isFieldPresent(sfIssuerEncryptedAmount)) + { + (*sleMptoken)[sfIssuerEncryptedBalance] = ctx_.tx[sfIssuerEncryptedAmount]; + setIssuerMirrorEpoch(*sleIssuance, *sleMptoken); + } + + if (ctx_.tx.isFieldPresent(sfAuditorEncryptedAmount)) + { + (*sleMptoken)[sfAuditorEncryptedBalance] = ctx_.tx[sfAuditorEncryptedAmount]; + setAuditorMirrorEpoch(*sleIssuance, *sleMptoken); + } + + view().update(sleMptoken); + return tesSUCCESS; +} + +void +ConfidentialMPTMirrorUpdate::visitInvariantEntry(bool, SLE::const_ref, SLE::const_ref) +{ +} + +bool +ConfidentialMPTMirrorUpdate::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/test/app/ConfidentialMPTKeyRotation_test.cpp b/src/test/app/ConfidentialMPTKeyRotation_test.cpp index 3f0f64d89d..0db9e29d27 100644 --- a/src/test/app/ConfidentialMPTKeyRotation_test.cpp +++ b/src/test/app/ConfidentialMPTKeyRotation_test.cpp @@ -3,9 +3,12 @@ #include #include +#include +#include #include #include #include +#include #include #include #include @@ -1259,6 +1262,1198 @@ class ConfidentialMPTKeyRotation_test : public ConfidentialTransferTestBase } } + void + testConfidentialMPTMirrorUpdatePreflight(FeatureBitset features) + { + testcase("ConfidentialMPTMirrorUpdate preflight"); + using namespace test::jtx; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + MPTTester mptAlice(env, alice, {.holders = {bob, carol}}); + + // A well-formed 66-byte ElGamal ciphertext + Buffer const& validCipher = getTrivialCiphertext(); + + // Both amendments are required: ConfidentialMPTKeyRotation and ConfidentialTransfer. + if (!features[featureConfidentialMPTKeyRotation] || !features[featureConfidentialTransfer]) + { + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); + mptAlice.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = validCipher, + .err = temDISABLED, + }); + return; + } + + mptAlice.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + // Issuer mode but account is not the issuer. + mptAlice.mirrorUpdate({ + .account = bob, + .holder = carol, + .issuerEncryptedAmount = validCipher, + .err = temMALFORMED, + }); + + // Issuer mode but the holder is the same as the issuer. + mptAlice.mirrorUpdate({ + .account = alice, + .holder = alice, + .issuerEncryptedAmount = validCipher, + .err = temMALFORMED, + }); + + // Issuer mode but holder is not provided. + mptAlice.mirrorUpdate({ + .account = alice, + .issuerEncryptedAmount = validCipher, + .err = temMALFORMED, + }); + + // At least one of issuer or auditor amount must be present. + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .err = temMALFORMED, + }); + + // Issuer amount has the wrong length. + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = gMakeZeroBuffer(10), + .err = temBAD_CIPHERTEXT, + }); + + // Auditor amount has the wrong length. + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .auditorEncryptedAmount = gMakeZeroBuffer(10), + .err = temBAD_CIPHERTEXT, + }); + + // The proof has the wrong length. + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .zkProof = gMakeZeroBuffer(kEcEqualityProofLength - 1), + .err = temMALFORMED, + }); + + // Issuer amount is the right length but not a valid ciphertext. + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = getBadCiphertext(), + .err = temBAD_CIPHERTEXT, + }); + + // Auditor amount is the right length but not a valid ciphertext. + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .auditorEncryptedAmount = getBadCiphertext(), + .err = temBAD_CIPHERTEXT, + }); + } + + void + testConfidentialMPTMirrorUpdatePreclaim(FeatureBitset features) + { + testcase("ConfidentialMPTMirrorUpdate preclaim"); + using namespace test::jtx; + + Buffer const& validCipher = getTrivialCiphertext(); + + // The issuance does not exist. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + + mptAlice.create({ + .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance, + }); + // Destroy the issuance to test issuance not found. + mptAlice.destroy(); + + mptAlice.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = validCipher, + .err = tecOBJECT_NOT_FOUND, + }); + } + + // The issuance has not enabled confidential balances. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create({.ownerCount = 1, .flags = tfMPTCanTransfer}); + + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // The issuer encryption key was not already registered. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}); + mptAlice.authorize({.account = bob}); + + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // In issuer mode, the specified holder account does not exist. + { + Env env{*this, features}; + Account const alice("alice"); + Account const carol("carol"); + MPTTester mptAlice(env, alice); + mptAlice.create( + {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}); + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + // Carol never got funded so it does not exist. + mptAlice.mirrorUpdate({ + .account = alice, + .holder = carol, + .issuerEncryptedAmount = validCipher, + .err = tecNO_TARGET, + }); + } + + // The holder's MPToken does not exist (holder never authorized). + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}); + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .err = tecOBJECT_NOT_FOUND, + }); + } + + // The holder has an MPToken but no confidential issuer balance (sfIssuerEncryptedBalance). + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + MPTTester mptAlice(env, alice, {.holders = {bob}}); + mptAlice.create( + {.ownerCount = 1, .flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance}); + mptAlice.authorize({.account = bob}); + mptAlice.generateKeyPair(alice); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(alice)}); + + mptAlice.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // Auditor mirror migration on an issuance with no auditor key. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + + // This setup has issuer key but no auditor key. + ConfidentialEnv ct{env, alice, {{.account = bob}}}; + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .auditorEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // Issuer mirror is already most up-to-date so + // there is nothing to migrate. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv ct{env, alice, {{.account = bob}}}; + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // Issuer-mode auditor-only migration while the issuer mirror is stale: + // the issuer mirror must be brought up to date before the auditor + // mirror can be migrated. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const newIssuerKey("newIssuerKey"); + + // Issuance has both an issuer key and an auditor key, and bob holds + // both mirrors at epoch 0. + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // Rotate the issuer key: issuer key epoch 0 -> 1, while bob's + // issuer-mirror epoch stays 0 (stale). + ct.mpt.generateKeyPair(newIssuerKey); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)}); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .auditorEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // Auditor mirror is already current (the auditor key has not rotated), + // so there is nothing to migrate. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + + // Issuance has both keys and bob holds both mirrors at epoch 0. + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // No key has rotated, so the auditor mirror is up to date + // so there is nothing to migrate. + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .auditorEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // In an issuer-mode simultaneous migration, both mirrors must be stale. Here + // only the issuer key has rotated so its mirror is stale but the auditor mirror is not. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const newIssuerKey("newIssuerKey"); + + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // Rotate only the issuer key: issuer key epoch 0 -> 1, auditor key + // epoch stays 0. The issuer mirror is now stale but the auditor + // mirror is still current. + ct.mpt.generateKeyPair(newIssuerKey); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)}); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .auditorEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // In an issuer-mode simultaneous migration, both mirrors must be stale. + // Here only the auditor key has rotated so its mirror is stale but the + // issuer mirror is not. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const newAuditorKey("newAuditorKey"); + + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // Rotate only the auditor key: auditor key epoch 0 -> 1, issuer key + // epoch stays 0. The auditor mirror is now stale but the issuer + // mirror is still current. + ct.mpt.generateKeyPair(newAuditorKey); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(newAuditorKey)}); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .auditorEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // Holder self-migration mode runs the same staleness checks. + // No key has rotated, so the holder's own issuer mirror is current and + // there is nothing to migrate. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + ConfidentialEnv ct{env, alice, {{.account = bob}}}; + + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // Holder self-migration mode, simultaneously migrating both keys: only the issuer key + // has rotated, so the holder's issuer mirror is stale but the auditor + // mirror is still current. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const newIssuerKey("newIssuerKey"); + + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // Rotate only the issuer key: issuer key epoch 0 -> 1, auditor key + // epoch stays 0. + ct.mpt.generateKeyPair(newIssuerKey); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)}); + + // Holder mode (no Holder field) needs no previous issuer key. + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = validCipher, + .auditorEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // Holder self-migration mode, simultaneously migrating both keys: + // only the auditor key has rotated, so the holder's auditor mirror is stale but the issuer + // mirror is still current. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const newAuditorKey("newAuditorKey"); + + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // Rotate only the auditor key: auditor key epoch 0 -> 1, issuer key + // epoch stays 0. + ct.mpt.generateKeyPair(newAuditorKey); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(newAuditorKey)}); + + // Auditor mirror is stale but issuer mirror is current so this is rejected. + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = validCipher, + .auditorEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + } + + // Holder self-migration requires the holder's inbox to be canonical + // zero, because the cross-key equality proof anchors on the spending + // balance, which only reflects the full balance after the inbox is + // merged. A holder with a non-zero inbox is rejected. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const newIssuerKey("newIssuerKey"); + + ConfidentialEnv ct{env, alice, {{.account = bob}, {.account = carol}}}; + + // Carol sends Bob a confidential amount; Bob does NOT merge it, so + // his inbox is no longer canonical zero. + ct.mpt.send({.account = carol, .dest = bob, .amt = 10}); + + // Rotate the issuer key so the issuer mirror is stale and the + // migration gets past the epoch check to reach the inbox check. + ct.mpt.generateKeyPair(newIssuerKey); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)}); + + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = validCipher, + .err = tecNO_PERMISSION, + }); + + // Merging the inbox makes the migration succeed. + ct.mpt.mergeInbox({.account = bob}); + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = validCipher, + .err = tesSUCCESS, + }); + } + + // A lock does not block a migration. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const newIssuerKey("newIssuerKey"); + Account const newerIssuerKey("newerIssuerKey"); + + ConfidentialEnv ct{env, alice, {{.account = bob}, {.account = carol}}}; + ct.mpt.set({.account = alice, .holder = bob, .flags = tfMPTLock}); + ct.mpt.set({.account = alice, .holder = carol, .flags = tfMPTLock}); + + // Rotate the issuer key so both holders' mirrors are stale. + ct.mpt.generateKeyPair(newIssuerKey); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)}); + + // The issuer migrates an individually locked holder. + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .err = tesSUCCESS, + }); + + // An individually locked holder migrates itself. + ct.mpt.mirrorUpdate({ + .account = carol, + .issuerEncryptedAmount = validCipher, + .err = tesSUCCESS, + }); + + // Release the individual locks and lock the whole issuance instead. Rotate again so + // both mirrors are stale once more. + ct.mpt.set({.account = alice, .holder = bob, .flags = tfMPTUnlock}); + ct.mpt.set({.account = alice, .holder = carol, .flags = tfMPTUnlock}); + ct.mpt.set({.account = alice, .flags = tfMPTLock}); + ct.mpt.generateKeyPair(newerIssuerKey); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newerIssuerKey)}); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = validCipher, + .err = tesSUCCESS, + }); + + ct.mpt.mirrorUpdate({ + .account = carol, + .issuerEncryptedAmount = validCipher, + .err = tesSUCCESS, + }); + } + } + + void + testConfidentialMPTMirrorUpdateDoApply(FeatureBitset features) + { + testcase("ConfidentialMPTMirrorUpdate doApply"); + using namespace test::jtx; + + // The holder's confidential balance, matching the ConfidentialEnv default + // convertAmount. The migration re-encrypts this amount under the new key. + std::uint64_t const amount = 100; + + // Issuer mode issuer-mirror migration. The new issuer mirror is written + // and the auditor mirror epoch advances to the issuer key epoch. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const newIssuerKey("newIssuerKey"); + ConfidentialEnv ct{env, alice, {{.account = bob}}}; + + // Rotate the issuer key: issuer key epoch 0 -> 1. + ct.mpt.generateKeyPair(newIssuerKey); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)}); + + // Re-encrypt Bob's balance under the new issuer key. + Buffer const newIssuerCipher = + ct.mpt.encryptAmount(newIssuerKey, amount, generateBlindingFactor()); + + // The previous issuer key is the pre-rotation issuer key (alice's), + // no longer on-ledger after the rotation, provide it in the transaction. + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = newIssuerCipher, + }); + + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 1u); + + // The issuer mirror is now current, so re-migrating it is rejected. + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = newIssuerCipher, + .err = tecNO_PERMISSION, + }); + } + + // Issuer mode auditor-mirror migration. The new auditor mirror is written + // and the auditor mirror epoch advances to the auditor key epoch. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const newAuditorKey("newAuditorKey"); + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // Rotate only the auditor key: auditor key epoch 0 -> 1. + ct.mpt.generateKeyPair(newAuditorKey); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(newAuditorKey)}); + + // Re-encrypt Bob's balance under the new auditor key. + Buffer const newAuditorCipher = + ct.mpt.encryptAmount(newAuditorKey, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .auditorEncryptedAmount = newAuditorCipher, + }); + + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher)); + BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 1u); + } + + // Issuer mode simultaneous migration: both mirrors are written in one transaction and + // both epochs advance. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const newIssuerKey("newIssuerKey"); + Account const newAuditorKey("newAuditorKey"); + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // Rotate both keys: both key epochs 0 -> 1. + ct.mpt.generateKeyPair(newIssuerKey); + ct.mpt.generateKeyPair(newAuditorKey); + ct.mpt.set({ + .account = alice, + .issuerPubKey = ct.mpt.getPubKey(newIssuerKey), + .auditorPubKey = ct.mpt.getPubKey(newAuditorKey), + }); + + // Re-encrypt Bob's balance under each new key. + Buffer const bf = generateBlindingFactor(); + Buffer const newIssuerCipher = ct.mpt.encryptAmount(newIssuerKey, amount, bf); + Buffer const newAuditorCipher = ct.mpt.encryptAmount(newAuditorKey, amount, bf); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = newIssuerCipher, + .auditorEncryptedAmount = newAuditorCipher, + }); + + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher)); + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 1u); + BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 1u); + } + + // Issuer mode auditor late-registration: the auditor key is registered for the first + // time (key epoch absent), so setting the initial auditor mirror leaves + // the auditor mirror epoch absent as well. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + // No auditor in the confidential setup, so bob has no auditor mirror. + ConfidentialEnv ct{env, alice, {{.account = bob}}}; + + // Register an auditor key for the first time (auditor key epoch stays + // absent). + ct.mpt.generateKeyPair(auditor); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditor)}); + + // Encrypt Bob's balance under the newly registered auditor key. + Buffer const auditorCipher = + ct.mpt.encryptAmount(auditor, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .auditorEncryptedAmount = auditorCipher, + }); + + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(auditorCipher)); + // First-time registration leaves the mirror epoch absent (== 0). + BEAST_EXPECT(!sle->isFieldPresent(sfAuditorKeyMirrorEpoch)); + } + + // Holder self-migration migrates from the holder's own spending balance + // (Holder being Account field, no Holder field, and no previous issuer key in any flow + // because the anchor is the spending balance, not the old issuer mirror). ConfidentialEnv + // already merged the inbox so the holder's inbox is canonical zero. + + // Holder issuer-mirror migration. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const newIssuerKey("newIssuerKey"); + ConfidentialEnv ct{env, alice, {{.account = bob}}}; + + ct.mpt.generateKeyPair(newIssuerKey); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(newIssuerKey)}); + + // The holder re-encrypts their own balance under the new issuer key. + Buffer const newIssuerCipher = + ct.mpt.encryptAmount(newIssuerKey, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = newIssuerCipher, + }); + + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 1u); + } + + // Holder auditor-mirror migration. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const newAuditorKey("newAuditorKey"); + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + ct.mpt.generateKeyPair(newAuditorKey); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(newAuditorKey)}); + + // The holder re-encrypts their own balance under the new auditor key. + Buffer const newAuditorCipher = + ct.mpt.encryptAmount(newAuditorKey, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = bob, + .auditorEncryptedAmount = newAuditorCipher, + }); + + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher)); + BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 1u); + } + + // Holder simultaneous migration of both mirrors. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const newIssuerKey("newIssuerKey"); + Account const newAuditorKey("newAuditorKey"); + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + ct.mpt.generateKeyPair(newIssuerKey); + ct.mpt.generateKeyPair(newAuditorKey); + ct.mpt.set({ + .account = alice, + .issuerPubKey = ct.mpt.getPubKey(newIssuerKey), + .auditorPubKey = ct.mpt.getPubKey(newAuditorKey), + }); + + Buffer const bf = generateBlindingFactor(); + Buffer const newIssuerCipher = ct.mpt.encryptAmount(newIssuerKey, amount, bf); + Buffer const newAuditorCipher = ct.mpt.encryptAmount(newAuditorKey, amount, bf); + + // Holder mode needs no previous issuer key even for the issuer mirror. + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = newIssuerCipher, + .auditorEncryptedAmount = newAuditorCipher, + }); + + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher)); + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 1u); + BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 1u); + } + + // Holder auditor late-registration: the auditor key is registered for the first time (key + // epoch absent), so the holder setting their initial auditor mirror leaves the auditor + // mirror epoch absent as well. + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + // No auditor in the confidential setup, so bob has no auditor mirror. + ConfidentialEnv ct{env, alice, {{.account = bob}}}; + + // Register an auditor key for the first time (auditor key epoch stays + // absent). + ct.mpt.generateKeyPair(auditor); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditor)}); + + // The holder encrypts their own balance under the newly registered auditor key. + Buffer const auditorCipher = + ct.mpt.encryptAmount(auditor, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = bob, + .auditorEncryptedAmount = auditorCipher, + }); + + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(auditorCipher)); + // First-time registration leaves the mirror epoch absent. + BEAST_EXPECT(!sle->isFieldPresent(sfAuditorKeyMirrorEpoch)); + } + } + + void + testConfidentialMPTMirrorUpdateMultipleRotationsIssuerMode(FeatureBitset features) + { + testcase("ConfidentialMPTMirrorUpdate issuer migrates after several rotations"); + using namespace test::jtx; + + std::uint64_t const amount = 100; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const issuerKey1("issuerKey1"); + Account const issuerKey2("issuerKey2"); + Account const issuerKey3("issuerKey3"); + Account const issuerKey4("issuerKey4"); + Account const issuerKey5("issuerKey5"); + Account const auditorKey1("auditorKey1"); + Account const auditorKey2("auditorKey2"); + Account const auditorKey3("auditorKey3"); + Account const auditorKey4("auditorKey4"); + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // Rotate the issuer key three times: issuer key epoch 0 -> 3. Bob never + // migrates in between, so his issuer mirror stays at mirror epoch 0 and + // is still encrypted under the original issuer key (alice's). + ct.mpt.generateKeyPair(issuerKey1); + ct.mpt.generateKeyPair(issuerKey2); + ct.mpt.generateKeyPair(issuerKey3); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey1)}); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey2)}); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey3)}); + + { + auto const sleIssuance = env.le(keylet::mptokenIssuance(ct.mpt.issuanceID())); + BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfIssuerKeyEpoch] == 3u); + } + + // A single migration re-encrypts the mirror under the newest key and + // jumps the mirror epoch straight to the current key epoch (3), rather + // than advancing one rotation at a time. The previous issuer key is the + // original key (alice's) that the stale mirror is still encrypted under, + // not any intermediate rotation. + Buffer const newIssuerCipher = + ct.mpt.encryptAmount(issuerKey3, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = newIssuerCipher, + }); + + { + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 3u); + } + + // The issuer mirror is now current (epoch 3 == key epoch 3), so a second + // issuer migration is rejected. + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = newIssuerCipher, + .err = tecNO_PERMISSION, + }); + + // Now rotate the auditor key twice: auditor key epoch 0 -> 2. Bob's + // auditor mirror is still at mirror epoch 0, under the original auditor + // key. The issuer key and its epoch are untouched. + ct.mpt.generateKeyPair(auditorKey1); + ct.mpt.generateKeyPair(auditorKey2); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditorKey1)}); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditorKey2)}); + + { + auto const sleIssuance = env.le(keylet::mptokenIssuance(ct.mpt.issuanceID())); + BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfAuditorKeyEpoch] == 2u); + BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfIssuerKeyEpoch] == 3u); + } + + // A single auditor-only migration jumps the auditor mirror epoch straight + // to the current auditor key epoch (2). This is an issuer-mode + // auditor-only migration, which is allowed because the issuer mirror is + // already current; no previous issuer key is needed for an auditor + // migration. + Buffer const newAuditorCipher = + ct.mpt.encryptAmount(auditorKey2, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .auditorEncryptedAmount = newAuditorCipher, + }); + + { + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher)); + BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 2u); + // The issuer mirror and its epoch are unaffected by the auditor + // migration. + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 3u); + } + + // The auditor mirror is now current (epoch 2 == key epoch 2), so a second + // auditor migration is rejected. + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .auditorEncryptedAmount = newAuditorCipher, + .err = tecNO_PERMISSION, + }); + + // Now rotate BOTH keys together twice: issuer key epoch 3 -> 5, auditor + // key epoch 2 -> 4. Bob's mirrors stay at epoch 3 / 2 (stale again). + ct.mpt.generateKeyPair(issuerKey4); + ct.mpt.generateKeyPair(issuerKey5); + ct.mpt.generateKeyPair(auditorKey3); + ct.mpt.generateKeyPair(auditorKey4); + ct.mpt.set({ + .account = alice, + .issuerPubKey = ct.mpt.getPubKey(issuerKey4), + .auditorPubKey = ct.mpt.getPubKey(auditorKey3), + }); + ct.mpt.set({ + .account = alice, + .issuerPubKey = ct.mpt.getPubKey(issuerKey5), + .auditorPubKey = ct.mpt.getPubKey(auditorKey4), + }); + + { + auto const sleIssuance = env.le(keylet::mptokenIssuance(ct.mpt.issuanceID())); + BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfIssuerKeyEpoch] == 5u); + BEAST_EXPECT(sleIssuance && (*sleIssuance)[~sfAuditorKeyEpoch] == 4u); + } + + // A single simultaneous migration brings both mirrors current in one + // transaction: issuer mirror epoch 3 -> 5, auditor mirror epoch 2 -> 4. + // The previous issuer key is issuerKey3, which is the key Bob's current + // (stale) issuer mirror is encrypted under after the earlier issuer + // migration, not alice's original key nor any intermediate rotation. + Buffer const bothIssuerCipher = + ct.mpt.encryptAmount(issuerKey5, amount, generateBlindingFactor()); + Buffer const bothAuditorCipher = + ct.mpt.encryptAmount(auditorKey4, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = bothIssuerCipher, + .auditorEncryptedAmount = bothAuditorCipher, + }); + + { + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(bothIssuerCipher)); + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(bothAuditorCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 5u); + BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 4u); + } + + // Both mirrors are current now, so a second simultaneous migration is + // rejected. + ct.mpt.mirrorUpdate({ + .account = alice, + .holder = bob, + .issuerEncryptedAmount = bothIssuerCipher, + .auditorEncryptedAmount = bothAuditorCipher, + .err = tecNO_PERMISSION, + }); + } + + void + testConfidentialMPTMirrorUpdateMultipleRotationsHolderMode(FeatureBitset features) + { + testcase("ConfidentialMPTMirrorUpdate holder migrates after several rotations"); + using namespace test::jtx; + + std::uint64_t const amount = 100; + + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + Account const auditor("auditor"); + Account const issuerKey1("issuerKey1"); + Account const issuerKey2("issuerKey2"); + Account const issuerKey3("issuerKey3"); + Account const issuerKey4("issuerKey4"); + Account const issuerKey5("issuerKey5"); + Account const auditorKey1("auditorKey1"); + Account const auditorKey2("auditorKey2"); + Account const auditorKey3("auditorKey3"); + Account const auditorKey4("auditorKey4"); + ConfidentialEnv ct{ + env, + alice, + {{.account = bob}}, + tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + auditor}; + + // In holder self-migration mode the holder submits (account = bob, no + // Holder field) and never provides a previous issuer key. + // Bob's inbox is canonical zero after the ConfidentialEnv merge. + + // Rotate the issuer key three times: issuer key epoch 0 -> 3. + ct.mpt.generateKeyPair(issuerKey1); + ct.mpt.generateKeyPair(issuerKey2); + ct.mpt.generateKeyPair(issuerKey3); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey1)}); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey2)}); + ct.mpt.set({.account = alice, .issuerPubKey = ct.mpt.getPubKey(issuerKey3)}); + + // A single holder migration jumps the issuer mirror epoch straight to 3. + Buffer const newIssuerCipher = + ct.mpt.encryptAmount(issuerKey3, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = newIssuerCipher, + }); + + { + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 3u); + } + + // The issuer mirror is current, so a second holder issuer migration is + // rejected. + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = newIssuerCipher, + .err = tecNO_PERMISSION, + }); + + // Rotate the auditor key twice: auditor key epoch 0 -> 2. + ct.mpt.generateKeyPair(auditorKey1); + ct.mpt.generateKeyPair(auditorKey2); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditorKey1)}); + ct.mpt.set({.account = alice, .auditorPubKey = ct.mpt.getPubKey(auditorKey2)}); + + // A single holder auditor migration jumps the auditor mirror epoch to 2. + Buffer const newAuditorCipher = + ct.mpt.encryptAmount(auditorKey2, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = bob, + .auditorEncryptedAmount = newAuditorCipher, + }); + + { + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(newAuditorCipher)); + BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 2u); + // The issuer mirror is unaffected. + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(newIssuerCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 3u); + } + + // The auditor mirror is current, so a second holder auditor migration is + // rejected. + ct.mpt.mirrorUpdate({ + .account = bob, + .auditorEncryptedAmount = newAuditorCipher, + .err = tecNO_PERMISSION, + }); + + // Rotate both keys together twice: issuer key epoch 3 -> 5, auditor key + // epoch 2 -> 4. + ct.mpt.generateKeyPair(issuerKey4); + ct.mpt.generateKeyPair(issuerKey5); + ct.mpt.generateKeyPair(auditorKey3); + ct.mpt.generateKeyPair(auditorKey4); + ct.mpt.set({ + .account = alice, + .issuerPubKey = ct.mpt.getPubKey(issuerKey4), + .auditorPubKey = ct.mpt.getPubKey(auditorKey3), + }); + ct.mpt.set({ + .account = alice, + .issuerPubKey = ct.mpt.getPubKey(issuerKey5), + .auditorPubKey = ct.mpt.getPubKey(auditorKey4), + }); + + // A single holder migration brings both mirrors current: issuer mirror + // epoch 3 -> 5, auditor mirror epoch 2 -> 4. Still no previous issuer key. + Buffer const bothIssuerCipher = + ct.mpt.encryptAmount(issuerKey5, amount, generateBlindingFactor()); + Buffer const bothAuditorCipher = + ct.mpt.encryptAmount(auditorKey4, amount, generateBlindingFactor()); + + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = bothIssuerCipher, + .auditorEncryptedAmount = bothAuditorCipher, + }); + + { + auto const sle = env.le(keylet::mptoken(ct.mpt.issuanceID(), bob.id())); + if (!BEAST_EXPECT(sle)) + return; + BEAST_EXPECT(strHex((*sle)[sfIssuerEncryptedBalance]) == strHex(bothIssuerCipher)); + BEAST_EXPECT(strHex((*sle)[sfAuditorEncryptedBalance]) == strHex(bothAuditorCipher)); + BEAST_EXPECT((*sle)[~sfIssuerKeyMirrorEpoch] == 5u); + BEAST_EXPECT((*sle)[~sfAuditorKeyMirrorEpoch] == 4u); + } + + // Both mirrors are current, so a second holder migration is rejected. + ct.mpt.mirrorUpdate({ + .account = bob, + .issuerEncryptedAmount = bothIssuerCipher, + .auditorEncryptedAmount = bothAuditorCipher, + .err = tecNO_PERMISSION, + }); + } + public: void testMPTokenIssuanceSetWithFeats(FeatureBitset features) @@ -1273,7 +2468,6 @@ public: testMPTokenIssuanceSetKeyEpochAtMax(features); } -public: void run() override { @@ -1288,6 +2482,14 @@ public: testConfidentialMPTSendEpoch(all); testConfidentialMPTConvertBackEpoch(all); testConfidentialMPTClawbackEpoch(all); + + testConfidentialMPTMirrorUpdatePreflight(all); + testConfidentialMPTMirrorUpdatePreflight(all - featureConfidentialMPTKeyRotation); + testConfidentialMPTMirrorUpdatePreflight(all - featureConfidentialTransfer); + testConfidentialMPTMirrorUpdatePreclaim(all); + testConfidentialMPTMirrorUpdateDoApply(all); + testConfidentialMPTMirrorUpdateMultipleRotationsIssuerMode(all); + testConfidentialMPTMirrorUpdateMultipleRotationsHolderMode(all); } }; diff --git a/src/test/app/ConfidentialTransfer_test.cpp b/src/test/app/ConfidentialTransfer_test.cpp index 9a0bf08660..9ff9270a7d 100644 --- a/src/test/app/ConfidentialTransfer_test.cpp +++ b/src/test/app/ConfidentialTransfer_test.cpp @@ -7131,6 +7131,18 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase mptAlice.confidentialClaw( {.account = alice, .holder = carol, .amt = 15, .fee = expectedFee}); }); + + // Check fee for the mirror update transaction. + Account const newIssuerKey("newIssuerKey"); + mptAlice.generateKeyPair(newIssuerKey); + mptAlice.set({.account = alice, .issuerPubKey = mptAlice.getPubKey(newIssuerKey)}); + checkFee(alice, [&]() { + mptAlice.mirrorUpdate( + {.account = alice, + .holder = bob, + .issuerEncryptedAmount = getTrivialCiphertext(), + .fee = expectedFee}); + }); } // test insufficient fee for confidential transactions @@ -7162,6 +7174,12 @@ class ConfidentialTransfer_test : public ConfidentialTransferTestBase .amt = 1, .fee = baseFee, .err = telINSUF_FEE_P}); + mptAlice.mirrorUpdate( + {.account = alice, + .holder = bob, + .issuerEncryptedAmount = getTrivialCiphertext(), + .fee = baseFee, + .err = telINSUF_FEE_P}); } // test excessive fee for confidential transactions diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 5414269333..5f57cfa21b 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -2838,7 +2838,7 @@ class Delegate_test : public beast::unit_test::Suite // DO NOT modify expectedDelegableCount unless all scenarios, including // edge cases, have been fully tested and verified. // ==================================================================== - std::size_t const expectedDelegableCount = 56; + std::size_t const expectedDelegableCount = 57; BEAST_EXPECTS( delegableCount == expectedDelegableCount, diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index 0853affab7..bb0c790e37 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -21,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -29,19 +29,14 @@ #include #include #include -#include #include #include #include #include #include -#include -#include #include -#include #include #include -#include #include #include @@ -268,136 +263,33 @@ enum class PeerFeature { * Simulate a network peer. * Depending on the configured PeerFeature, * it either supports the ProtocolFeature::LedgerReplay or not + * + * `PeerStub` supplies the rest of the `Peer` interface as no-ops. */ -class TestPeer : public Peer +class TestPeer : public PeerStub { public: - TestPeer(bool enableLedgerReplay) - : ledgerReplayEnabled_(enableLedgerReplay) - , nodePublicKey_(derivePublicKey(KeyType::Ed25519, randomSecretKey())) + // Arbitrary but fixed: the replay code only compares ids. + explicit TestPeer(bool enableLedgerReplay) + : PeerStub(1234), ledgerReplayEnabled_(enableLedgerReplay) { } - void - send(std::shared_ptr const& m) override - { - } - [[nodiscard]] beast::ip::Endpoint - getRemoteAddress() const override - { - return {}; - } - void - charge(resource::Charge const& fee, std::string const& context = {}) override - { - } - [[nodiscard]] id_t - id() const override - { - return 1234; - } - [[nodiscard]] bool - cluster() const override - { - return false; - } - [[nodiscard]] bool - isHighLatency() const override - { - return false; - } - [[nodiscard]] int - getScore(bool) const override - { - return 0; - } - [[nodiscard]] PublicKey const& - getNodePublic() const override - { - return nodePublicKey_; - } - json::Value - json() override - { - return {}; - } [[nodiscard]] bool supportsFeature(ProtocolFeature f) const override { return f == ProtocolFeature::LedgerReplay && ledgerReplayEnabled_; } - [[nodiscard]] std::optional - publisherListSequence(PublicKey const&) const override - { - return {}; - } - void - setPublisherListSequence(PublicKey const&, std::size_t const) override - { - } - [[nodiscard]] uint256 - getClosedLedgerHash() const override - { - static uint256 const kHash{}; - return kHash; - } + + // The replay code only asks peers that already have the ledger. [[nodiscard]] bool - hasLedger(uint256 const& hash, std::uint32_t seq) const override + hasLedger(uint256 const&, std::uint32_t) const override { return true; } - void - ledgerRange(std::uint32_t& minSeq, std::uint32_t& maxSeq) const override - { - } - [[nodiscard]] bool - hasTxSet(uint256 const& hash) const override - { - return false; - } - void - cycleStatus() override - { - } - bool - hasRange(std::uint32_t uMin, std::uint32_t uMax) override - { - return false; - } - [[nodiscard]] bool - compressionEnabled() const override - { - return false; - } - void - sendTxQueue() override - { - } - void - addTxQueue(uint256 const&) override - { - } - void - removeTxQueue(uint256 const&) override - { - } - [[nodiscard]] bool - txReduceRelayEnabled() const override - { - return false; - } - [[nodiscard]] std::string const& - fingerprint() const override - { - return fingerprint_; - } - - // NOLINTBEGIN(readability-identifier-naming) - std::string fingerprint_; +private: bool ledgerReplayEnabled_; - PublicKey nodePublicKey_; - // NOLINTEND(readability-identifier-naming) }; enum class PeerSetBehavior { diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 71d968014f..22b5d0bcdc 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -1163,6 +1163,25 @@ public: sponsor::SponseeAcc(alice), Ter(temMALFORMED)); } + + // Post-fixCleanup3_5_0, a zero ObjectID is malformed. + // Pre-fixCleanup3_5_0 path is unreachable so it is not testable. + if (features[fixCleanup3_5_0]) + { + uint256 const zeroObjectID{}; + + env(sponsor::transfer(alice, tfSponsorshipEnd, zeroObjectID), Ter(temMALFORMED)); + + env(sponsor::transfer(alice, tfSponsorshipCreate, zeroObjectID), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(temMALFORMED)); + + env(sponsor::transfer(alice, tfSponsorshipReassign, zeroObjectID), + sponsor::As(sponsor, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor), + Ter(temMALFORMED)); + } } { diff --git a/src/test/jtx/ConfidentialTransfer.h b/src/test/jtx/ConfidentialTransfer.h index 5c1b90328b..21e1929902 100644 --- a/src/test/jtx/ConfidentialTransfer.h +++ b/src/test/jtx/ConfidentialTransfer.h @@ -3,28 +3,19 @@ #include #include #include -#include #include -#include #include #include -#include #include #include #include #include #include -#include - -#include -#include - #include #include #include -#include #include #include #include @@ -54,237 +45,66 @@ protected: return *value; } - // Offset where the bulletproof begins in a send proof blob. - // Proof layout: [compact_sigma | bulletproof] - static constexpr size_t kBulletproofOffset = kEcSendProofLength - kEcDoubleBulletproofLength; - - // Generate a forged aggregated bulletproof (double bulletproof) for - // the given values and blinding factors. Used to test that splicing - // a bulletproof claiming a different remaining balance is rejected. - // secp256k1 convention: returns 1 on success, 0 on failure. - static Buffer - getForgedBulletproof( - std::array const& values, - std::array const& blindingFactors, - uint256 const& contextHash) + // Creates the MPT issuance on the given Env, authorizes and funds each + // holder, generates keys for the issuer, holders and optional auditor, + // registers the issuer/auditor keys, and converts part of each holder's + // balance to a confidential balance. + struct ConfidentialEnv { - auto* const ctx = mpt_secp256k1_context(); + // Per-holder configuration: the account, how much MPT to fund it + // with, and how much of that to convert to a confidential balance. + struct HolderInit + { + test::jtx::Account account; + std::uint64_t payAmount = 1000; + std::uint64_t convertAmount = 100; + }; - secp256k1_pubkey h; - secp256k1_mpt_get_h_generator(ctx, &h); + test::jtx::MPTTester mpt; - Buffer proof(kEcDoubleBulletproofLength); - size_t proofLen = kEcDoubleBulletproofLength; + ConfidentialEnv( + test::jtx::Env& env, + test::jtx::Account const& issuer, + std::vector const& holders, + std::uint32_t flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, + std::optional auditor = std::nullopt); - unsigned char blindings[64]; - std::memcpy(blindings, blindingFactors[0].data(), 32); - std::memcpy(blindings + 32, blindingFactors[1].data(), 32); + private: + static std::vector + extractAccounts(std::vector const& holders); + }; - if (secp256k1_bulletproof_prove_agg( - ctx, - proof.data(), - &proofLen, - values.data(), - blindings, - 2, - &h, - contextHash.data()) == 0) - Throw("Failed to generate forged bulletproof"); - - return proof; - } - - // Generate a forged single bulletproof for a single value and blinding factor. - // Used to test ConvertBack overdraft prevention via bulletproof verification. - static Buffer - getForgedSingleBulletproof( - uint64_t value, - Buffer const& blindingFactor, - uint256 const& contextHash) - { - auto* const ctx = mpt_secp256k1_context(); - - secp256k1_pubkey h; - secp256k1_mpt_get_h_generator(ctx, &h); - - Buffer proof(kEcSingleBulletproofLength); - size_t proofLen = kEcSingleBulletproofLength; - - if (secp256k1_bulletproof_prove_agg( - ctx, - proof.data(), - &proofLen, - &value, - blindingFactor.data(), - 1, // m = 1 (single bulletproof) - &h, - contextHash.data()) == 0) - Throw("Failed to generate forged single bulletproof"); - - return proof; - } - - // Forges a ConvertBack proof (compact sigma + single bulletproof) whose - // sigma component claims claimedBalance (which may be wrong) while binding - // to the real pedersen commitment and encrypted spending balance - // ciphertext already on the ledger. The bulletproof component is built - // from realBalance so it stays honest. - // mpt_get_convert_back_proof does not allow to build a proof whose amount - // exceeds the holder's claimed balance. - static Buffer - getForgedConvertBackProof( + // 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& holder, - uint64_t claimedBalance, - uint64_t realBalance, - uint64_t amt, - Buffer const& pedersenCommitment, - Buffer const& encryptedSpendingBalance, - Buffer const& pcBlindingFactor, - uint256 const& contextHash) - { - if (pedersenCommitment.size() != kCompressedEcPointLength) - Throw("getForgedConvertBackProof: bad pedersenCommitment length"); - if (encryptedSpendingBalance.size() != kEcGamalEncryptedTotalLength) - { - Throw( - "getForgedConvertBackProof: bad encryptedSpendingBalance length"); - } - if (amt > realBalance) - Throw("getForgedConvertBackProof: amt exceeds realBalance"); + test::jtx::Account const& issuer, + std::vector const& holders, + std::vector const& keyOwners = {}, + std::uint32_t flags = tfMPTCanTransfer | tfMPTCanHoldConfidentialBalance); - auto* const ctx = mpt_secp256k1_context(); - auto const holderPubKey = requireOptional(mpt.getPubKey(holder), "Missing holder pubkey"); - auto const holderPrivKey = - requireOptional(mpt.getPrivKey(holder), "Missing holder privkey"); - - secp256k1_pubkey pkHolder; - if (secp256k1_ec_pubkey_parse( - ctx, &pkHolder, holderPubKey.data(), kCompressedEcPointLength) != 1) - Throw("Failed to parse holder's public key"); - - secp256k1_pubkey pcB; - if (secp256k1_ec_pubkey_parse( - ctx, &pcB, pedersenCommitment.data(), kCompressedEcPointLength) != 1) - Throw("Failed to parse pedersen commitment"); - - secp256k1_pubkey b1, b2; - if (secp256k1_ec_pubkey_parse( - ctx, &b1, encryptedSpendingBalance.data(), kCompressedEcPointLength) != 1 || - secp256k1_ec_pubkey_parse( - ctx, - &b2, - encryptedSpendingBalance.data() + kCompressedEcPointLength, - kCompressedEcPointLength) != 1) - Throw("Failed to parse balance ciphertext"); - - Buffer sigmaProof(SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); - if (secp256k1_compact_convertback_prove( - ctx, - sigmaProof.data(), - claimedBalance, - holderPrivKey.data(), - pcBlindingFactor.data(), - &pkHolder, - &b1, - &b2, - &pcB, - contextHash.data()) != 1) - Throw("Failed to generate convertback sigma proof"); - - auto const forgedBulletproof = - getForgedSingleBulletproof(realBalance - amt, pcBlindingFactor, contextHash); - - Buffer proof(kEcConvertBackProofLength); - std::memcpy(proof.data(), sigmaProof.data(), SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); - std::memcpy( - proof.data() + SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE, - forgedBulletproof.data(), - kEcSingleBulletproofLength); - - return proof; - } - - // Get a bad ciphertext with valid structure but cryptographic invalid for - // testing purposes. For preflight test purposes. - static Buffer const& - getBadCiphertext() - { - static Buffer const kBadCiphertext = []() { - Buffer buf(kEcGamalEncryptedTotalLength); - std::memset(buf.data(), 0xFF, kEcGamalEncryptedTotalLength); - - buf.data()[0] = kEcCompressedPrefixEvenY; - buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY; - return buf; - }(); - - return kBadCiphertext; - } - - // Get a trivial buffer that is structurally and mathematically valid, but - // contains invalid data that does not match the ledger state. For preclaim - // test purposes. - static Buffer const& - getTrivialCiphertext() - { - static Buffer const kTrivialCiphertext = []() { - Buffer buf(kEcGamalEncryptedTotalLength); - std::memset(buf.data(), 0, kEcGamalEncryptedTotalLength); - - buf.data()[0] = kEcCompressedPrefixEvenY; - buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY; - - buf.data()[kEcCiphertextComponentLength - 1] = 0x01; - buf.data()[kEcGamalEncryptedTotalLength - 1] = 0x01; - - return buf; - }(); - - return kTrivialCiphertext; - } - - // Returns a valid compressed EC point (33 bytes) that can pass preflight - // validation but contains invalid data for preclaim test purposes. - static Buffer const& - getTrivialCommitment() - { - static Buffer const kTrivialCommitment = []() { - Buffer buf(kEcPedersenCommitmentLength); - std::memset(buf.data(), 0, kEcPedersenCommitmentLength); - - buf.data()[0] = kEcCompressedPrefixEvenY; - // Set last byte to make it a valid x-coordinate on the curve - buf.data()[kEcPedersenCommitmentLength - 1] = 0x01; - - return buf; - }(); - - return kTrivialCommitment; - } - - static std::string - getTrivialSendProofHex() - { - Buffer buf(kEcSendProofLength); - std::memset(buf.data(), 0, kEcSendProofLength); - - for (std::size_t i = 0; i < kEcSendProofLength; i += kEcCiphertextComponentLength) - { - buf.data()[i] = kEcCompressedPrefixEvenY; - if (i + kEcCiphertextComponentLength - 1 < kEcSendProofLength) - buf.data()[i + kEcCiphertextComponentLength - 1] = 0x01; - } - - return strHex(buf); - } + // 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 + // zero spending/inbox. + static void + setupBatchEnv( + test::jtx::MPTTester& mpt, + test::jtx::Account const& alice, + test::jtx::Account const& bob, + test::jtx::Account const& carol, + test::jtx::Account const& dave, + std::uint64_t bobAmt, + std::uint64_t carolAmt); // Helper struct to encapsulate common setup for integration tests. struct ConfidentialSendSetup { // Constants uint64_t sendAmount; - size_t nRecipients; uint32_t version; // Blinding factors @@ -324,55 +144,7 @@ protected: test::jtx::Account const& dest, test::jtx::Account const& issuer, uint64_t amount, - std::optional> auditor = std::nullopt) - : sendAmount(amount) - , nRecipients(auditor ? 4 : 3) - , version(mpt.getMPTokenVersion(sender)) - , blindingFactor(generateBlindingFactor()) - , amountBlindingFactor(blindingFactor) - , balanceBlindingFactor(generateBlindingFactor()) - , senderAmt(mpt.encryptAmount(sender, amount, blindingFactor)) - , destAmt(mpt.encryptAmount(dest, amount, blindingFactor)) - , issuerAmt(mpt.encryptAmount(issuer, amount, blindingFactor)) - , auditorAmt( - auditor ? std::optional( - mpt.encryptAmount(auditor->get(), amount, blindingFactor)) - : std::nullopt) - , amountCommitment(mpt.getPedersenCommitment(amount, amountBlindingFactor)) - , senderPubKey(requireOptional(mpt.getPubKey(sender), "Missing sender public key")) - , destPubKey(requireOptional(mpt.getPubKey(dest), "Missing destination public key")) - , issuerPubKey(requireOptional(mpt.getPubKey(issuer), "Missing issuer public key")) - , auditorPubKey(auditor ? mpt.getPubKey(auditor->get()) : std::nullopt) - , prevSpending(requireOptional( - mpt.getDecryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending), - "Missing sender spending balance")) - , prevEncryptedSpending(requireOptional( - mpt.getEncryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending), - "Missing sender encrypted spending balance")) - , balanceCommitment(mpt.getPedersenCommitment(prevSpending, balanceBlindingFactor)) - { - recipients.push_back({ - .publicKey = Slice(senderPubKey), - .encryptedAmount = senderAmt, - }); - recipients.push_back({ - .publicKey = Slice(destPubKey), - .encryptedAmount = destAmt, - }); - recipients.push_back({ - .publicKey = Slice(issuerPubKey), - .encryptedAmount = issuerAmt, - }); - if (auditor) - { - recipients.push_back({ - .publicKey = - Slice(requireOptionalRef(auditorPubKey, "Missing auditor public key")), - .encryptedAmount = - requireOptionalRef(auditorAmt, "Missing auditor encrypted amount"), - }); - } - } + std::optional> auditor = std::nullopt); // Generate proof with current account sequence std::optional @@ -380,54 +152,78 @@ protected: test::jtx::MPTTester& mpt, test::jtx::Env& env, test::jtx::Account const& sender, - test::jtx::Account const& dest) const - { - auto const ctxHash = getSendContextHash( - sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), version); - - return mpt.getConfidentialSendProof( - sender, - sendAmount, - recipients, - blindingFactor, - ctxHash, - { - .pedersenCommitment = amountCommitment, - .amt = sendAmount, - .encryptedAmt = senderAmt, - .blindingFactor = amountBlindingFactor, - }, - { - .pedersenCommitment = balanceCommitment, - .amt = prevSpending, - .encryptedAmt = prevEncryptedSpending, - .blindingFactor = balanceBlindingFactor, - }); - } + test::jtx::Account const& dest) const; [[nodiscard]] test::jtx::MPTConfidentialSend sendArgs( test::jtx::Account const& sender, test::jtx::Account const& dest, Buffer const& proof, - std::optional err = std::nullopt) const - { - return { - .account = sender, - .dest = dest, - .amt = sendAmount, - .proof = strHex(proof), - .senderEncryptedAmt = senderAmt, - .destEncryptedAmt = destAmt, - .issuerEncryptedAmt = issuerAmt, - .auditorEncryptedAmt = auditorAmt, - .amountCommitment = amountCommitment, - .balanceCommitment = balanceCommitment, - .err = err, - }; - } + std::optional err = std::nullopt) const; }; + // Get a bad ciphertext with valid structure but cryptographic invalid for + // testing purposes. For preflight test purposes. + static Buffer const& + getBadCiphertext(); + + // Get a trivial buffer that is structurally and mathematically valid, but + // contains invalid data that does not match the ledger state. For preclaim + // test purposes. + static Buffer const& + getTrivialCiphertext(); + + // Returns a valid compressed EC point (33 bytes) that can pass preflight + // validation but contains invalid data for preclaim test purposes. + static Buffer const& + getTrivialCommitment(); + + // Returns a hex-encoded send proof of the correct length filled with + // placeholder data. It passes the proof length check in preflight but + // fails proof verification. + static std::string + getTrivialSendProofHex(); + + // Offset where the bulletproof begins in a send proof blob. + // Proof layout: [compact_sigma | bulletproof] + static constexpr size_t kBulletproofOffset = kEcSendProofLength - kEcDoubleBulletproofLength; + + // Generate a forged aggregated bulletproof (double bulletproof) for + // the given values and blinding factors. Used to test that splicing + // a bulletproof claiming a different remaining balance is rejected. + static Buffer + getForgedBulletproof( + std::array const& values, + std::array const& blindingFactors, + uint256 const& contextHash); + + // Generate a forged single bulletproof for a single value and blinding factor. + // Used to test ConvertBack overdraft prevention via bulletproof verification. + static Buffer + getForgedSingleBulletproof( + uint64_t value, + Buffer const& blindingFactor, + uint256 const& contextHash); + + // Forges a ConvertBack proof (compact sigma + single bulletproof) whose + // sigma component claims claimedBalance (which may be wrong) while binding + // to the real pedersen commitment and to the encrypted spending balance + // already on the ledger. The bulletproof component is built from the real + // remaining balance (realBalance - amt) so it stays honest. + // mpt_get_convert_back_proof validates its inputs before proving, so it + // cannot be used to build such an inconsistent proof. + static Buffer + getForgedConvertBackProof( + test::jtx::MPTTester& mpt, + test::jtx::Account const& holder, + uint64_t claimedBalance, + uint64_t realBalance, + uint64_t amt, + Buffer const& pedersenCommitment, + Buffer const& encryptedSpendingBalance, + Buffer const& pcBlindingFactor, + uint256 const& contextHash); + // Forges a ConfidentialMPTSend proof (compact sigma + double bulletproof) // for setup.sendAmount against setup's real balance commitment/ciphertext. // mpt_get_confidential_send_proof does not allow to build a proof whose amount @@ -438,265 +234,7 @@ protected: test::jtx::Env& env, test::jtx::Account const& sender, test::jtx::Account const& dest, - ConfidentialSendSetup const& setup) - { - auto* const ctx = mpt_secp256k1_context(); - - secp256k1_pubkey c1; - std::vector c2Vec(setup.recipients.size()); - std::vector pkVec(setup.recipients.size()); - for (std::size_t i = 0; i < setup.recipients.size(); ++i) - { - auto const& r = setup.recipients[i]; - if (i == 0 && - secp256k1_ec_pubkey_parse( - ctx, &c1, r.encryptedAmount.data(), kCompressedEcPointLength) != 1) - Throw("Failed to parse C1"); - if (secp256k1_ec_pubkey_parse( - ctx, - &c2Vec[i], - r.encryptedAmount.data() + kCompressedEcPointLength, - kCompressedEcPointLength) != 1) - Throw("Failed to parse C2"); - if (secp256k1_ec_pubkey_parse( - ctx, &pkVec[i], r.publicKey.data(), kCompressedEcPointLength) != 1) - Throw("Failed to parse recipient pubkey"); - } - - secp256k1_pubkey pkSender, pcAmount, pcBalance, b1, b2; - if (secp256k1_ec_pubkey_parse( - ctx, &pkSender, setup.senderPubKey.data(), kCompressedEcPointLength) != 1 || - secp256k1_ec_pubkey_parse( - ctx, &pcAmount, setup.amountCommitment.data(), kCompressedEcPointLength) != 1 || - secp256k1_ec_pubkey_parse( - ctx, &pcBalance, setup.balanceCommitment.data(), kCompressedEcPointLength) != 1 || - secp256k1_ec_pubkey_parse( - ctx, &b1, setup.prevEncryptedSpending.data(), kCompressedEcPointLength) != 1 || - secp256k1_ec_pubkey_parse( - ctx, - &b2, - setup.prevEncryptedSpending.data() + kCompressedEcPointLength, - kCompressedEcPointLength) != 1) - Throw("Failed to parse commitments/ciphertext"); - - Buffer const senderPrivKey = - requireOptional(mpt.getPrivKey(sender), "Missing sender privkey"); - auto const ctxHash = getSendContextHash( - sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), setup.version); - - Buffer sigmaProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE); - if (secp256k1_compact_standard_prove( - ctx, - sigmaProof.data(), - setup.sendAmount, - setup.prevSpending, - setup.blindingFactor.data(), - senderPrivKey.data(), - setup.balanceBlindingFactor.data(), - setup.recipients.size(), - &c1, - c2Vec.data(), - pkVec.data(), - &pcAmount, - &pkSender, - &pcBalance, - &b1, - &b2, - ctxHash.data()) != 1) - Throw("Failed to generate sigma proof"); - - // Wraps (mod 2^64) for overdrafts, unlike the ledger's own homomorphic - // commitment subtraction (mod the curve order) — that mismatch is - // exactly what makes the forged proof fail verification. - // Computed without a wrapping `uint64` subtract: Clang UBSan treats - // unsigned overflow as fatal (see incrementConfidentialVersion). - std::uint64_t const remaining = setup.sendAmount <= setup.prevSpending - ? setup.prevSpending - setup.sendAmount - : ~setup.sendAmount + setup.prevSpending + 1; - - Buffer negAmountBf(kEcBlindingFactorLength); - Buffer remainingBf(kEcBlindingFactorLength); - secp256k1_mpt_scalar_negate(negAmountBf.data(), setup.amountBlindingFactor.data()); - secp256k1_mpt_scalar_add( - remainingBf.data(), setup.balanceBlindingFactor.data(), negAmountBf.data()); - - auto const forgedBulletproof = getForgedBulletproof( - {setup.sendAmount, remaining}, {setup.amountBlindingFactor, remainingBf}, ctxHash); - - Buffer combinedProof(kEcSendProofLength); - std::memcpy(combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_STANDARD_PROOF_SIZE); - std::memcpy( - combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE, - forgedBulletproof.data(), - kEcDoubleBulletproofLength); - - return combinedProof; - } - - // Helper that wraps the boilerplate setup: Env + MPT creation, funding, key - // generation, and seeding each holder with a confidential balance. - // The caller supplies the issuer and any number of holders. - struct ConfidentialEnv - { - // Per-holder configuration: the account, how much MPT to fund it - // with, and how much of that to convert to a confidential balance. - struct HolderInit - { - test::jtx::Account account; - std::uint64_t payAmount = 1000; - std::uint64_t convertAmount = 100; - }; - - test::jtx::MPTTester mpt; - - ConfidentialEnv( - test::jtx::Env& env, - test::jtx::Account const& issuer, - std::vector const& holders, - std::uint32_t flags = tfMPTCanLock | tfMPTCanHoldConfidentialBalance | tfMPTCanTransfer, - std::optional auditor = std::nullopt) - : mpt{env, issuer, {.holders = extractAccounts(holders), .auditor = auditor}} - { - mpt.create({.ownerCount = 1, .flags = flags}); - - for (auto const& h : holders) - { - mpt.authorize({.account = h.account}); - if ((flags & tfMPTRequireAuth) != 0) - mpt.authorize({.account = issuer, .holder = h.account}); - mpt.pay(issuer, h.account, h.payAmount); - } - - mpt.generateKeyPair(issuer); - for (auto const& h : holders) - mpt.generateKeyPair(h.account); - if (auditor) - mpt.generateKeyPair(requireOptionalRef(auditor, "Missing auditor")); - - mpt.set({ - .account = issuer, - .issuerPubKey = mpt.getPubKey(issuer), - .auditorPubKey = auditor - ? mpt.getPubKey(requireOptionalRef(auditor, "Missing auditor")) - : std::optional{}, - }); - - for (auto const& h : holders) - { - mpt.convert({ - .account = h.account, - .amt = h.convertAmount, - .holderPubKey = mpt.getPubKey(h.account), - }); - mpt.mergeInbox({.account = h.account}); - } - } - - private: - static std::vector - extractAccounts(std::vector const& holders) - { - std::vector accounts; - accounts.reserve(holders.size()); - for (auto const& h : holders) - accounts.push_back(h.account); - return accounts; - } - }; - - // 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 - // zero spending/inbox. - static void - setupBatchEnv( - test::jtx::MPTTester& mpt, - test::jtx::Account const& alice, - test::jtx::Account const& bob, - test::jtx::Account const& carol, - test::jtx::Account const& dave, - std::uint64_t bobAmt, - std::uint64_t carolAmt) - { - using namespace test::jtx; - mpt.create({ - .ownerCount = 1, - .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, - }); - mpt.authorize({.account = bob}); - mpt.authorize({.account = carol}); - mpt.authorize({.account = dave}); - - if (bobAmt > 0) - mpt.pay(alice, bob, bobAmt); - if (carolAmt > 0) - mpt.pay(alice, carol, carolAmt); - - mpt.generateKeyPair(alice); - mpt.generateKeyPair(bob); - mpt.generateKeyPair(carol); - mpt.generateKeyPair(dave); - - mpt.set({ - .account = alice, - .issuerPubKey = mpt.getPubKey(alice), - }); - - if (bobAmt > 0) - { - mpt.convert({ - .account = bob, - .amt = bobAmt, - .holderPubKey = mpt.getPubKey(bob), - }); - mpt.mergeInbox({.account = bob}); - } - else - { - mpt.convert({ - .account = bob, - .amt = 0, - .holderPubKey = mpt.getPubKey(bob), - }); - } - - if (carolAmt > 0) - { - mpt.convert({ - .account = carol, - .amt = carolAmt, - .holderPubKey = mpt.getPubKey(carol), - }); - mpt.mergeInbox({.account = carol}); - } - else - { - mpt.convert({ - .account = carol, - .amt = 0, - .holderPubKey = mpt.getPubKey(carol), - }); - } - - // dave: register pubkey only (0 spending/inbox) - mpt.convert({ - .account = dave, - .amt = 0, - .holderPubKey = mpt.getPubKey(dave), - }); - } + ConfidentialSendSetup const& setup); }; } // namespace xrpl diff --git a/src/test/jtx/PeerStub.h b/src/test/jtx/PeerStub.h new file mode 100644 index 0000000000..cabe94f351 --- /dev/null +++ b/src/test/jtx/PeerStub.h @@ -0,0 +1,185 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace xrpl::test { + +/** + * A `Peer` whose every method is a no-op returning a default. + * + * Derive from this and override only the methods a test cares about. Adding a + * method to `Peer` then costs one stub here, not one per double. + * + * The id and the node public key are real, because the code under test routes + * and deduplicates on both. + */ +class PeerStub : public Peer +{ +public: + /** + * @param id The connection id reported by `id()`. + */ + explicit PeerStub(id_t id = 0) + : id_(id), nodePublicKey_(derivePublicKey(KeyType::Ed25519, randomSecretKey())) + { + } + + ~PeerStub() override = default; + + void + send(std::shared_ptr const&) override + { + } + + [[nodiscard]] beast::ip::Endpoint + getRemoteAddress() const override + { + return {}; + } + + void + sendTxQueue() override + { + } + + void + addTxQueue(uint256 const&) override + { + } + + void + removeTxQueue(uint256 const&) override + { + } + + void + charge(resource::Charge const&, std::string const&) override + { + } + + [[nodiscard]] id_t + id() const override + { + return id_; + } + + [[nodiscard]] bool + cluster() const override + { + return false; + } + + [[nodiscard]] bool + isHighLatency() const override + { + return false; + } + + [[nodiscard]] int + getScore(bool) const override + { + return 0; + } + + [[nodiscard]] PublicKey const& + getNodePublic() const override + { + return nodePublicKey_; + } + + json::Value + json() override + { + return {}; + } + + [[nodiscard]] bool + supportsFeature(ProtocolFeature) const override + { + return false; + } + + [[nodiscard]] std::optional + publisherListSequence(PublicKey const&) const override + { + return {}; + } + + void + setPublisherListSequence(PublicKey const&, std::size_t const) override + { + } + + [[nodiscard]] std::string const& + fingerprint() const override + { + return fingerprint_; + } + + [[nodiscard]] uint256 + getClosedLedgerHash() const override + { + return {}; + } + + [[nodiscard]] bool + hasLedger(uint256 const&, std::uint32_t) const override + { + return false; + } + + void + ledgerRange(std::uint32_t&, std::uint32_t&) const override + { + } + + [[nodiscard]] bool + hasTxSet(uint256 const&) const override + { + return false; + } + + void + cycleStatus() override + { + } + + bool + hasRange(std::uint32_t, std::uint32_t) override + { + return false; + } + + [[nodiscard]] bool + compressionEnabled() const override + { + return false; + } + + [[nodiscard]] bool + txReduceRelayEnabled() const override + { + return false; + } + +private: + id_t const id_; + PublicKey const nodePublicKey_; + std::string const fingerprint_; +}; + +} // namespace xrpl::test diff --git a/src/test/jtx/impl/ConfidentialTransfer.cpp b/src/test/jtx/impl/ConfidentialTransfer.cpp index 6c1538316c..0bc98778f6 100644 --- a/src/test/jtx/impl/ConfidentialTransfer.cpp +++ b/src/test/jtx/impl/ConfidentialTransfer.cpp @@ -1,13 +1,89 @@ #include #include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include #include +#include +#include +#include +#include +#include #include namespace xrpl { +ConfidentialTransferTestBase::ConfidentialEnv::ConfidentialEnv( + test::jtx::Env& env, + test::jtx::Account const& issuer, + std::vector const& holders, + std::uint32_t flags, + std::optional auditor) + : mpt{env, issuer, {.holders = extractAccounts(holders), .auditor = auditor}} +{ + mpt.create({.ownerCount = 1, .flags = flags}); + + for (auto const& h : holders) + { + mpt.authorize({.account = h.account}); + if ((flags & tfMPTRequireAuth) != 0) + mpt.authorize({.account = issuer, .holder = h.account}); + mpt.pay(issuer, h.account, h.payAmount); + } + + mpt.generateKeyPair(issuer); + for (auto const& h : holders) + mpt.generateKeyPair(h.account); + if (auditor) + mpt.generateKeyPair(requireOptionalRef(auditor, "Missing auditor")); + + mpt.set({ + .account = issuer, + .issuerPubKey = mpt.getPubKey(issuer), + .auditorPubKey = auditor ? mpt.getPubKey(requireOptionalRef(auditor, "Missing auditor")) + : std::optional{}, + }); + + for (auto const& h : holders) + { + mpt.convert({ + .account = h.account, + .amt = h.convertAmount, + .holderPubKey = mpt.getPubKey(h.account), + }); + mpt.mergeInbox({.account = h.account}); + } +} + +std::vector +ConfidentialTransferTestBase::ConfidentialEnv::extractAccounts( + std::vector const& holders) +{ + std::vector accounts; + accounts.reserve(holders.size()); + for (auto const& h : holders) + accounts.push_back(h.account); + return accounts; +} + void ConfidentialTransferTestBase::setupConfidentialIssuance( test::jtx::MPTTester& mpt, @@ -34,4 +110,478 @@ ConfidentialTransferTestBase::setupConfidentialIssuance( mpt.generateKeyPair(keyOwner); } +void +ConfidentialTransferTestBase::setupBatchEnv( + test::jtx::MPTTester& mpt, + test::jtx::Account const& alice, + test::jtx::Account const& bob, + test::jtx::Account const& carol, + test::jtx::Account const& dave, + std::uint64_t bobAmt, + std::uint64_t carolAmt) +{ + using namespace test::jtx; + mpt.create({ + .ownerCount = 1, + .flags = tfMPTCanTransfer | tfMPTCanLock | tfMPTCanHoldConfidentialBalance, + }); + mpt.authorize({.account = bob}); + mpt.authorize({.account = carol}); + mpt.authorize({.account = dave}); + + if (bobAmt > 0) + mpt.pay(alice, bob, bobAmt); + if (carolAmt > 0) + mpt.pay(alice, carol, carolAmt); + + mpt.generateKeyPair(alice); + mpt.generateKeyPair(bob); + mpt.generateKeyPair(carol); + mpt.generateKeyPair(dave); + + mpt.set({ + .account = alice, + .issuerPubKey = mpt.getPubKey(alice), + }); + + if (bobAmt > 0) + { + mpt.convert({ + .account = bob, + .amt = bobAmt, + .holderPubKey = mpt.getPubKey(bob), + }); + mpt.mergeInbox({.account = bob}); + } + else + { + mpt.convert({ + .account = bob, + .amt = 0, + .holderPubKey = mpt.getPubKey(bob), + }); + } + + if (carolAmt > 0) + { + mpt.convert({ + .account = carol, + .amt = carolAmt, + .holderPubKey = mpt.getPubKey(carol), + }); + mpt.mergeInbox({.account = carol}); + } + else + { + mpt.convert({ + .account = carol, + .amt = 0, + .holderPubKey = mpt.getPubKey(carol), + }); + } + + // dave: register pubkey only (0 spending/inbox) + mpt.convert({ + .account = dave, + .amt = 0, + .holderPubKey = mpt.getPubKey(dave), + }); +} + +ConfidentialTransferTestBase::ConfidentialSendSetup::ConfidentialSendSetup( + test::jtx::MPTTester& mpt, + test::jtx::Account const& sender, + test::jtx::Account const& dest, + test::jtx::Account const& issuer, + uint64_t amount, + std::optional> auditor) + : sendAmount(amount) + , version(mpt.getMPTokenVersion(sender)) + , blindingFactor(generateBlindingFactor()) + , amountBlindingFactor(blindingFactor) + , balanceBlindingFactor(generateBlindingFactor()) + , senderAmt(mpt.encryptAmount(sender, amount, blindingFactor)) + , destAmt(mpt.encryptAmount(dest, amount, blindingFactor)) + , issuerAmt(mpt.encryptAmount(issuer, amount, blindingFactor)) + , auditorAmt( + auditor ? std::optional(mpt.encryptAmount(auditor->get(), amount, blindingFactor)) + : std::nullopt) + , amountCommitment(mpt.getPedersenCommitment(amount, amountBlindingFactor)) + , senderPubKey(requireOptional(mpt.getPubKey(sender), "Missing sender public key")) + , destPubKey(requireOptional(mpt.getPubKey(dest), "Missing destination public key")) + , issuerPubKey(requireOptional(mpt.getPubKey(issuer), "Missing issuer public key")) + , auditorPubKey(auditor ? mpt.getPubKey(auditor->get()) : std::nullopt) + , prevSpending(requireOptional( + mpt.getDecryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending), + "Missing sender spending balance")) + , prevEncryptedSpending(requireOptional( + mpt.getEncryptedBalance(sender, test::jtx::MPTTester::holderEncryptedSpending), + "Missing sender encrypted spending balance")) + , balanceCommitment(mpt.getPedersenCommitment(prevSpending, balanceBlindingFactor)) +{ + recipients.push_back({ + .publicKey = Slice(senderPubKey), + .encryptedAmount = senderAmt, + }); + recipients.push_back({ + .publicKey = Slice(destPubKey), + .encryptedAmount = destAmt, + }); + recipients.push_back({ + .publicKey = Slice(issuerPubKey), + .encryptedAmount = issuerAmt, + }); + if (auditor) + { + recipients.push_back({ + .publicKey = Slice(requireOptionalRef(auditorPubKey, "Missing auditor public key")), + .encryptedAmount = requireOptionalRef(auditorAmt, "Missing auditor encrypted amount"), + }); + } +} + +std::optional +ConfidentialTransferTestBase::ConfidentialSendSetup::generateProof( + test::jtx::MPTTester& mpt, + test::jtx::Env& env, + test::jtx::Account const& sender, + test::jtx::Account const& dest) const +{ + auto const ctxHash = + getSendContextHash(sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), version); + + return mpt.getConfidentialSendProof( + sender, + sendAmount, + recipients, + blindingFactor, + ctxHash, + { + .pedersenCommitment = amountCommitment, + .amt = sendAmount, + .encryptedAmt = senderAmt, + .blindingFactor = amountBlindingFactor, + }, + { + .pedersenCommitment = balanceCommitment, + .amt = prevSpending, + .encryptedAmt = prevEncryptedSpending, + .blindingFactor = balanceBlindingFactor, + }); +} + +test::jtx::MPTConfidentialSend +ConfidentialTransferTestBase::ConfidentialSendSetup::sendArgs( + test::jtx::Account const& sender, + test::jtx::Account const& dest, + Buffer const& proof, + std::optional err) const +{ + return { + .account = sender, + .dest = dest, + .amt = sendAmount, + .proof = strHex(proof), + .senderEncryptedAmt = senderAmt, + .destEncryptedAmt = destAmt, + .issuerEncryptedAmt = issuerAmt, + .auditorEncryptedAmt = auditorAmt, + .amountCommitment = amountCommitment, + .balanceCommitment = balanceCommitment, + .err = err, + }; +} + +Buffer const& +ConfidentialTransferTestBase::getBadCiphertext() +{ + static Buffer const kBadCiphertext = []() { + Buffer buf(kEcGamalEncryptedTotalLength); + std::memset(buf.data(), 0xFF, kEcGamalEncryptedTotalLength); + + buf.data()[0] = kEcCompressedPrefixEvenY; + buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY; + return buf; + }(); + + return kBadCiphertext; +} + +Buffer const& +ConfidentialTransferTestBase::getTrivialCiphertext() +{ + static Buffer const kTrivialCiphertext = []() { + Buffer buf(kEcGamalEncryptedTotalLength); + std::memset(buf.data(), 0, kEcGamalEncryptedTotalLength); + + buf.data()[0] = kEcCompressedPrefixEvenY; + buf.data()[kEcCiphertextComponentLength] = kEcCompressedPrefixEvenY; + + buf.data()[kEcCiphertextComponentLength - 1] = 0x01; + buf.data()[kEcGamalEncryptedTotalLength - 1] = 0x01; + + return buf; + }(); + + return kTrivialCiphertext; +} + +Buffer const& +ConfidentialTransferTestBase::getTrivialCommitment() +{ + static Buffer const kTrivialCommitment = []() { + Buffer buf(kEcPedersenCommitmentLength); + std::memset(buf.data(), 0, kEcPedersenCommitmentLength); + + buf.data()[0] = kEcCompressedPrefixEvenY; + // Set last byte to make it a valid x-coordinate on the curve + buf.data()[kEcPedersenCommitmentLength - 1] = 0x01; + + return buf; + }(); + + return kTrivialCommitment; +} + +std::string +ConfidentialTransferTestBase::getTrivialSendProofHex() +{ + Buffer buf(kEcSendProofLength); + std::memset(buf.data(), 0, kEcSendProofLength); + + for (std::size_t i = 0; i < kEcSendProofLength; i += kEcCiphertextComponentLength) + { + buf.data()[i] = kEcCompressedPrefixEvenY; + if (i + kEcCiphertextComponentLength - 1 < kEcSendProofLength) + buf.data()[i + kEcCiphertextComponentLength - 1] = 0x01; + } + + return strHex(buf); +} + +Buffer +ConfidentialTransferTestBase::getForgedBulletproof( + std::array const& values, + std::array const& blindingFactors, + uint256 const& contextHash) +{ + auto* const ctx = mpt_secp256k1_context(); + + secp256k1_pubkey h; + secp256k1_mpt_get_h_generator(ctx, &h); + + Buffer proof(kEcDoubleBulletproofLength); + size_t proofLen = kEcDoubleBulletproofLength; + + unsigned char blindings[64]; + std::memcpy(blindings, blindingFactors[0].data(), 32); + std::memcpy(blindings + 32, blindingFactors[1].data(), 32); + + if (secp256k1_bulletproof_prove_agg( + ctx, proof.data(), &proofLen, values.data(), blindings, 2, &h, contextHash.data()) == 0) + Throw("Failed to generate forged bulletproof"); + + return proof; +} + +Buffer +ConfidentialTransferTestBase::getForgedSingleBulletproof( + uint64_t value, + Buffer const& blindingFactor, + uint256 const& contextHash) +{ + auto* const ctx = mpt_secp256k1_context(); + + secp256k1_pubkey h; + secp256k1_mpt_get_h_generator(ctx, &h); + + Buffer proof(kEcSingleBulletproofLength); + size_t proofLen = kEcSingleBulletproofLength; + + if (secp256k1_bulletproof_prove_agg( + ctx, + proof.data(), + &proofLen, + &value, + blindingFactor.data(), + 1, // m = 1 (single bulletproof) + &h, + contextHash.data()) == 0) + Throw("Failed to generate forged single bulletproof"); + + return proof; +} + +Buffer +ConfidentialTransferTestBase::getForgedConvertBackProof( + test::jtx::MPTTester& mpt, + test::jtx::Account const& holder, + uint64_t claimedBalance, + uint64_t realBalance, + uint64_t amt, + Buffer const& pedersenCommitment, + Buffer const& encryptedSpendingBalance, + Buffer const& pcBlindingFactor, + uint256 const& contextHash) +{ + if (pedersenCommitment.size() != kCompressedEcPointLength) + Throw("getForgedConvertBackProof: bad pedersenCommitment length"); + if (encryptedSpendingBalance.size() != kEcGamalEncryptedTotalLength) + { + Throw("getForgedConvertBackProof: bad encryptedSpendingBalance length"); + } + if (amt > realBalance) + Throw("getForgedConvertBackProof: amt exceeds realBalance"); + + auto* const ctx = mpt_secp256k1_context(); + auto const holderPubKey = requireOptional(mpt.getPubKey(holder), "Missing holder pubkey"); + auto const holderPrivKey = requireOptional(mpt.getPrivKey(holder), "Missing holder privkey"); + + secp256k1_pubkey pkHolder; + if (secp256k1_ec_pubkey_parse(ctx, &pkHolder, holderPubKey.data(), kCompressedEcPointLength) != + 1) + Throw("Failed to parse holder's public key"); + + secp256k1_pubkey pcB; + if (secp256k1_ec_pubkey_parse(ctx, &pcB, pedersenCommitment.data(), kCompressedEcPointLength) != + 1) + Throw("Failed to parse pedersen commitment"); + + secp256k1_pubkey b1, b2; + if (secp256k1_ec_pubkey_parse( + ctx, &b1, encryptedSpendingBalance.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, + &b2, + encryptedSpendingBalance.data() + kCompressedEcPointLength, + kCompressedEcPointLength) != 1) + Throw("Failed to parse balance ciphertext"); + + Buffer sigmaProof(SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); + if (secp256k1_compact_convertback_prove( + ctx, + sigmaProof.data(), + claimedBalance, + holderPrivKey.data(), + pcBlindingFactor.data(), + &pkHolder, + &b1, + &b2, + &pcB, + contextHash.data()) != 1) + Throw("Failed to generate convertback sigma proof"); + + auto const forgedBulletproof = + getForgedSingleBulletproof(realBalance - amt, pcBlindingFactor, contextHash); + + Buffer proof(kEcConvertBackProofLength); + std::memcpy(proof.data(), sigmaProof.data(), SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE); + std::memcpy( + proof.data() + SECP256K1_COMPACT_CONVERTBACK_PROOF_SIZE, + forgedBulletproof.data(), + kEcSingleBulletproofLength); + + return proof; +} + +Buffer +ConfidentialTransferTestBase::getForgedSendProof( + test::jtx::MPTTester& mpt, + test::jtx::Env& env, + test::jtx::Account const& sender, + test::jtx::Account const& dest, + ConfidentialSendSetup const& setup) +{ + auto* const ctx = mpt_secp256k1_context(); + + secp256k1_pubkey c1; + std::vector c2Vec(setup.recipients.size()); + std::vector pkVec(setup.recipients.size()); + for (std::size_t i = 0; i < setup.recipients.size(); ++i) + { + auto const& r = setup.recipients[i]; + if (i == 0 && + secp256k1_ec_pubkey_parse( + ctx, &c1, r.encryptedAmount.data(), kCompressedEcPointLength) != 1) + Throw("Failed to parse C1"); + if (secp256k1_ec_pubkey_parse( + ctx, + &c2Vec[i], + r.encryptedAmount.data() + kCompressedEcPointLength, + kCompressedEcPointLength) != 1) + Throw("Failed to parse C2"); + if (secp256k1_ec_pubkey_parse( + ctx, &pkVec[i], r.publicKey.data(), kCompressedEcPointLength) != 1) + Throw("Failed to parse recipient pubkey"); + } + + secp256k1_pubkey pkSender, pcAmount, pcBalance, b1, b2; + if (secp256k1_ec_pubkey_parse( + ctx, &pkSender, setup.senderPubKey.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, &pcAmount, setup.amountCommitment.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, &pcBalance, setup.balanceCommitment.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, &b1, setup.prevEncryptedSpending.data(), kCompressedEcPointLength) != 1 || + secp256k1_ec_pubkey_parse( + ctx, + &b2, + setup.prevEncryptedSpending.data() + kCompressedEcPointLength, + kCompressedEcPointLength) != 1) + Throw("Failed to parse commitments/ciphertext"); + + Buffer const senderPrivKey = requireOptional(mpt.getPrivKey(sender), "Missing sender privkey"); + auto const ctxHash = getSendContextHash( + sender.id(), mpt.issuanceID(), env.seq(sender), dest.id(), setup.version); + + Buffer sigmaProof(SECP256K1_COMPACT_STANDARD_PROOF_SIZE); + if (secp256k1_compact_standard_prove( + ctx, + sigmaProof.data(), + setup.sendAmount, + setup.prevSpending, + setup.blindingFactor.data(), + senderPrivKey.data(), + setup.balanceBlindingFactor.data(), + setup.recipients.size(), + &c1, + c2Vec.data(), + pkVec.data(), + &pcAmount, + &pkSender, + &pcBalance, + &b1, + &b2, + ctxHash.data()) != 1) + Throw("Failed to generate sigma proof"); + + // Wraps (mod 2^64) for overdrafts, unlike the ledger's own homomorphic + // commitment subtraction (mod the curve order) — that mismatch is + // exactly what makes the forged proof fail verification. + // Computed without a wrapping `uint64` subtract: Clang UBSan treats + // unsigned overflow as fatal (see incrementConfidentialVersion). + std::uint64_t const remaining = setup.sendAmount <= setup.prevSpending + ? setup.prevSpending - setup.sendAmount + : ~setup.sendAmount + setup.prevSpending + 1; + + Buffer negAmountBf(kEcBlindingFactorLength); + Buffer remainingBf(kEcBlindingFactorLength); + secp256k1_mpt_scalar_negate(negAmountBf.data(), setup.amountBlindingFactor.data()); + secp256k1_mpt_scalar_add( + remainingBf.data(), setup.balanceBlindingFactor.data(), negAmountBf.data()); + + auto const forgedBulletproof = getForgedBulletproof( + {setup.sendAmount, remaining}, {setup.amountBlindingFactor, remainingBf}, ctxHash); + + Buffer combinedProof(kEcSendProofLength); + std::memcpy(combinedProof.data(), sigmaProof.data(), SECP256K1_COMPACT_STANDARD_PROOF_SIZE); + std::memcpy( + combinedProof.data() + SECP256K1_COMPACT_STANDARD_PROOF_SIZE, + forgedBulletproof.data(), + kEcDoubleBulletproofLength); + + return combinedProof; +} + } // namespace xrpl diff --git a/src/test/jtx/impl/mpt.cpp b/src/test/jtx/impl/mpt.cpp index 512257cdc5..542043015b 100644 --- a/src/test/jtx/impl/mpt.cpp +++ b/src/test/jtx/impl/mpt.cpp @@ -2171,4 +2171,33 @@ MPTTester::convertBackJV(MPTConvertBack const& arg, std::uint32_t seq) return jv; } +void +MPTTester::mirrorUpdate(MPTMirrorUpdate const& arg) +{ + json::Value jv; + jv[jss::TransactionType] = jss::ConfidentialMPTMirrorUpdate; + + setAccountField(jv, arg.account); + setIssuanceIdField(jv, arg.id); + + if (arg.holder) + jv[sfHolder] = arg.holder->human(); + if (arg.issuerEncryptedAmount) + jv[sfIssuerEncryptedAmount] = strHex(*arg.issuerEncryptedAmount); + if (arg.auditorEncryptedAmount) + jv[sfAuditorEncryptedAmount] = strHex(*arg.auditorEncryptedAmount); + + // Placeholder for proof, the logic will be added in the future + if (arg.zkProof) + { + jv[sfZKProof] = strHex(*arg.zkProof); + } + else + { + jv[sfZKProof] = strHex(gMakeZeroBuffer(kEcEqualityProofLength)); + } + + submit(arg, jv); +} + } // namespace xrpl::test::jtx diff --git a/src/test/jtx/impl/utility.cpp b/src/test/jtx/impl/utility.cpp index f83cb7772c..6b2c9b69b9 100644 --- a/src/test/jtx/impl/utility.cpp +++ b/src/test/jtx/impl/utility.cpp @@ -78,7 +78,7 @@ fillFee(json::Value& jv, ReadView const& view) auto const txType = jv[jss::TransactionType].asString(); if (txType == jss::ConfidentialMPTConvert || txType == jss::ConfidentialMPTConvertBack || txType == jss::ConfidentialMPTSend || txType == jss::ConfidentialMPTMergeInbox || - txType == jss::ConfidentialMPTClawback) + txType == jss::ConfidentialMPTClawback || txType == jss::ConfidentialMPTMirrorUpdate) { jv[jss::Fee] = to_string(base * (kConfidentialFeeMultiplier + 1)); } diff --git a/src/test/jtx/mpt.h b/src/test/jtx/mpt.h index cefdd2cdca..a737e76320 100644 --- a/src/test/jtx/mpt.h +++ b/src/test/jtx/mpt.h @@ -362,6 +362,24 @@ struct MPTConfidentialClawback std::optional err = std::nullopt; }; +/** + * @brief Arguments for building a ConfidentialMPTMirrorUpdate test transaction. + */ +struct MPTMirrorUpdate +{ + std::optional account = std::nullopt; + std::optional holder = std::nullopt; + std::optional id = std::nullopt; + std::optional issuerEncryptedAmount = std::nullopt; + std::optional auditorEncryptedAmount = std::nullopt; + std::optional zkProof = std::nullopt; + std::optional fee = std::nullopt; + std::optional flags = std::nullopt; + std::optional ownerCount = std::nullopt; + std::optional holderCount = std::nullopt; + std::optional err = std::nullopt; +}; + /** * @brief Stores the parameters that are exclusively used to generate a * Pedersen linkage proof. @@ -584,6 +602,9 @@ public: void confidentialClaw(MPTConfidentialClawback const& arg = MPTConfidentialClawback{}); + void + mirrorUpdate(MPTMirrorUpdate const& arg = MPTMirrorUpdate{}); + [[nodiscard]] bool checkDomainID(std::optional expected) const; diff --git a/src/test/overlay/CapturePeer.h b/src/test/overlay/CapturePeer.h new file mode 100644 index 0000000000..28a26d01e7 --- /dev/null +++ b/src/test/overlay/CapturePeer.h @@ -0,0 +1,252 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl::test { + +/** + * A real `PeerImp` that captures the messages it would have sent. + * + * Only `send` and `run` are overridden, so `onMessage` runs production code. + * Derive from this to reach a `protected` `PeerImp` member. + */ +class CapturePeer : public PeerImp +{ +public: + using MiddleType = boost::beast::tcp_stream; + using StreamType = boost::beast::ssl_stream; + using SocketType = boost::asio::ip::tcp::socket; + + /** + * Takes `PeerImp`'s two rvalue-reference parameters by value instead, so a + * derived double can inherit this constructor without a never-moved-from + * warning. + * + * @param app The application owning the peer. + * @param id The connection id, unique among the overlay's peers. + * @param slot The peer finder slot; must be seated. + * @param request The handshake request. + * @param publicKey The peer's node public key. + * @param protocol The negotiated protocol version. + * @param consumer The resource manager endpoint for the peer. + * @param streamPtr The connection's ssl stream. + * @param overlay The overlay to register with. + */ + CapturePeer( + Application& app, + Peer::id_t id, + std::shared_ptr const& slot, + http_request_type request, + PublicKey const& publicKey, + ProtocolVersion protocol, + resource::Consumer consumer, + std::unique_ptr streamPtr, + OverlayImpl& overlay) + : PeerImp( + app, + id, + slot, + std::move(request), + publicKey, + protocol, + consumer, // copy-only, so `std::move` would be a copy anyway + std::move(streamPtr), + overlay) + { + } + + ~CapturePeer() override = default; + + /** + * Does nothing, so the peer stays registered. The real `run()` reaches + * `PeerImp::doAccept`, which fails on an unconnected socket and detaches. + */ + void + run() override + { + } + + /** + * Captures the message instead of writing it, so replies are observable. + */ + void + send(std::shared_ptr const& m) override + { + sent_.push_back(m); + } + + /** + * @return Every message sent to this peer, in order. + */ + std::vector> const& + sent() const + { + return sent_; + } + + /** + * @return The most recent message sent, or null if there was none. + */ + std::shared_ptr + lastSent() const + { + return sent_.empty() ? nullptr : sent_.back(); + } + + /** + * Reads the accumulated charge without draining it through `charge()`. + * + * @return The charge accumulated on the peer so far. + */ + resource::Charge + feeCharge() const + { + return currentFeeCharge(); + } + +private: + std::vector> sent_; +}; + +namespace detail { + +// `inline` so the functions below name one entity across translation units. +inline constexpr std::uint16_t kCapturePeerPort = 51235; + +/** + * Non-template, so all `makeCapturePeer` instantiations share one counter. A + * per-instantiation counter would give two peer types the same id, and + * `addActive` would silently drop the second from `ids_`. + * + * @return The next unused connection id. + */ +inline Peer::id_t +nextCapturePeerId() +{ + static Peer::id_t id{0}; + return ++id; +} + +/** + * Non-template for the same reason as `nextCapturePeerId`. Each peer needs its + * own address, not just its own port: the peer finder caps inbound connections + * per address at `ipLimit`, which is at most 2 unless configured. + * + * @return The next unused remote endpoint. + */ +inline beast::ip::Endpoint +nextCapturePeerRemote() +{ + // From 172.2.0.1 upward, so ~900k fit before reaching 172.16/12, where the + // peer finder would treat them as private rather than as real inbound. + static std::uint32_t next{0xAC020001}; + return beast::ip::Endpoint(boost::asio::ip::address_v4(next++), kCapturePeerPort); +} + +/** + * Outlives every peer, whose stream keeps a reference to it. `PeerImp::charge` + * posts a handler holding the peer, so a peer can outlive its caller's scope. + * + * @return The ssl context every test peer's stream is built on. + */ +inline boost::asio::ssl::context& +capturePeerSslContext() +{ + static std::shared_ptr const kContext{makeSslContext("")}; + return *kContext; +} + +} // namespace detail + +/** + * Build an active `CapturePeer` and register it with the overlay. + * + * @tparam PeerType The peer class to build; must derive from `CapturePeer` and + * inherit its constructor. + * @param env The environment owning the overlay. + * @param key The peer's node public key, or unseated for a fresh random + * one. + * @param request The handshake request. `PeerImp` reads its `X-Protocol-Ctl` + * header in the constructor to negotiate features. + * @return The peer, already registered with the overlay. Throws if the peer + * finder refused a slot. + */ +template +std::shared_ptr +makeCapturePeer( + jtx::Env& env, + std::optional key = std::nullopt, + http_request_type request = {}) +{ + auto& overlay = dynamic_cast(env.app().getOverlay()); + auto streamPtr = std::make_unique( + CapturePeer::SocketType(env.app().getIOContext()), detail::capturePeerSslContext()); + + beast::ip::Endpoint const local( + boost::asio::ip::make_address("172.1.1.1"), detail::kCapturePeerPort); + auto const remote = detail::nextCapturePeerRemote(); + + auto consumer = overlay.resourceManager().newInboundEndpoint(remote); + auto [slot, _] = overlay.peerFinder().newInboundSlot(local, remote); + + // Unseated when the endpoint is already connected or at the per-address + // limit. `PeerImp` dereferences the slot, so fail here, not there. + if (!slot) + { + Throw("makeCapturePeer: no slot for " + to_string(remote)); + } + + if (!key) + key = PublicKey(std::get<0>(randomKeyPair(KeyType::Ed25519))); + + auto peer = std::make_shared( + env.app(), + detail::nextCapturePeerId(), + slot, + std::move(request), + *key, + // An unsupported version fails every `supportsFeature` test, so a + // version-gated reply would only ever take its legacy branch. + newestSupportedProtocolVersion(), + consumer, + std::move(streamPtr), + overlay); + + overlay.addActive(peer); + return peer; +} + +} // namespace xrpl::test diff --git a/src/test/overlay/PeerTest.cpp b/src/test/overlay/PeerTest.cpp deleted file mode 100644 index 341febb25b..0000000000 --- a/src/test/overlay/PeerTest.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include - -namespace xrpl::test { - -PeerTest::PeerTest( - Application& app, - std::shared_ptr const& slot, - http_request_type&& request, - PublicKey const& publicKey, - ProtocolVersion protocol, - resource::Consumer consumer, - std::unique_ptr&& streamPtr, - OverlayImpl& overlay) - : PeerImp{ - app, - id++, - slot, - std::move(request), - publicKey, - protocol, - consumer, - std::move(streamPtr), - overlay} -{ -} - -void -PeerTest::run() -{ -} - -void -PeerTest::send(std::shared_ptr const& message) -{ - lastSentMessage_ = message; -} - -std::shared_ptr -PeerTest::getLastSentMessage() const -{ - return lastSentMessage_; -} - -void -PeerTest::runProcessGetObjectByHash(std::shared_ptr const& message) -{ - PeerImp::processGetObjectByHash(message); -} - -void -PeerTest::runProcessLedgerRequest( - std::shared_ptr const& message, - std::vector nodeIDs) -{ - PeerImp::processLedgerRequest(message, std::move(nodeIDs)); -} - -resource::Charge -PeerTest::getCurrentFeeCharge() const -{ - return PeerImp::currentFeeCharge(); -} - -void -PeerTest::resetId() -{ - id = 0; -} - -bool -PeerTest::compressionEnabled() const -{ - if (compressionEnabled_.has_value()) - { - return *compressionEnabled_; - } - return PeerImp::compressionEnabled(); -} - -void -PeerTest::compressionEnabled(std::optional enabled) -{ - compressionEnabled_ = enabled; -} - -bool -PeerTest::txReduceRelayEnabled() const -{ - if (reduceRelayEnabled_.has_value()) - { - return *reduceRelayEnabled_; - } - return PeerImp::txReduceRelayEnabled(); -} - -void -PeerTest::txReduceRelayEnabled(std::optional enabled) -{ - reduceRelayEnabled_ = enabled; -} - -std::shared_ptr -makePeerTest(jtx::Env& env, PeerTest::SharedContext const& context, ProtocolVersion protocolVersion) -{ - using SocketType = boost::asio::ip::tcp::socket; - - auto& overlay = dynamic_cast(env.app().getOverlay()); - boost::beast::http::request request; - auto streamPtr = - std::make_unique(SocketType(env.app().getIOContext()), *context); - - beast::ip::Endpoint const local(boost::asio::ip::make_address("172.1.1.1"), 51235); - beast::ip::Endpoint const remote(boost::asio::ip::make_address("172.1.1.2"), 51235); - - PublicKey const key{std::get<0>(randomKeyPair(KeyType::Ed25519))}; - auto consumer = overlay.resourceManager().newInboundEndpoint(remote); - auto [slot, _] = overlay.peerFinder().newInboundSlot(local, remote); - - auto peer = std::make_shared( - env.app(), - slot, - std::move(request), - key, - protocolVersion, - consumer, - std::move(streamPtr), - overlay); - - overlay.addActive(peer); - return peer; -} - -} // namespace xrpl::test diff --git a/src/test/overlay/PeerTest.h b/src/test/overlay/PeerTest.h deleted file mode 100644 index f7b2815da3..0000000000 --- a/src/test/overlay/PeerTest.h +++ /dev/null @@ -1,108 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -namespace xrpl::test { - -/** - * Test peer that captures sent messages for verification. - */ -class PeerTest : public PeerImp -{ - inline static Peer::id_t id{}; - std::shared_ptr lastSentMessage_; - std::optional compressionEnabled_; - std::optional reduceRelayEnabled_; - -public: - using MiddleType = boost::beast::tcp_stream; - using SharedContext = std::shared_ptr; - using StreamType = boost::beast::ssl_stream; - - PeerTest( - Application& app, - std::shared_ptr const& slot, - http_request_type&& request, - PublicKey const& publicKey, - ProtocolVersion protocol, - resource::Consumer consumer, - std::unique_ptr&& streamPtr, - OverlayImpl& overlay); - - ~PeerTest() override = default; - - void - run() override; - - void - send(std::shared_ptr const& m) override; - - std::shared_ptr - getLastSentMessage() const; - - // Synchronous test access to the JobQueue-dispatched processor. - // The production path runs this on JtLedgerReq; tests need a - // synchronous entry point to inspect the reply via send(). - // PeerImp::processGetObjectByHash is `protected` so the derived - // test subclass can call it directly. - void - runProcessGetObjectByHash(std::shared_ptr const& m); - - void - runProcessLedgerRequest( - std::shared_ptr const& m, - std::vector nodeIDs); - - resource::Charge - getCurrentFeeCharge() const; - - static void - resetId(); - - bool - compressionEnabled() const override; - - void - compressionEnabled(std::optional enabled); - - bool - txReduceRelayEnabled() const override; - - void - txReduceRelayEnabled(std::optional enabled); -}; - -std::shared_ptr -makePeerTest( - jtx::Env& env, - PeerTest::SharedContext const& context, - ProtocolVersion protocolVersion); - -} // namespace xrpl::test diff --git a/src/test/overlay/TMGetLedger_test.cpp b/src/test/overlay/TMGetLedger_test.cpp index 9088e6fa65..eac1a24e24 100644 --- a/src/test/overlay/TMGetLedger_test.cpp +++ b/src/test/overlay/TMGetLedger_test.cpp @@ -1,30 +1,19 @@ #include -#include +#include #include -#include -#include -#include -#include #include #include -#include #include -#include #include #include -#include -#include -#include -#include -#include - #include #include #include +#include #include namespace xrpl::test { @@ -33,8 +22,23 @@ using namespace jtx; class TMGetLedger_test : public beast::unit_test::Suite { - PeerTest::SharedContext context_{makeSslContext("")}; - ProtocolVersion protocolVersion_{1, 7}; + /** + * Calls the JtLedgerReq-dispatched processor synchronously, so the reply is + * visible through `lastSent()`. + */ + class GetLedgerPeer : public CapturePeer + { + public: + using CapturePeer::CapturePeer; + + void + runProcessLedgerRequest( + std::shared_ptr const& m, + std::vector nodeIDs) + { + processLedgerRequest(m, std::move(nodeIDs)); + } + }; // Build a well-formed TMGetLedger node request carrying `numNodeIds` node // IDs. @@ -64,17 +68,16 @@ class TMGetLedger_test : public beast::unit_test::Suite testcase("Node ID Count Accepted"); Env env{*this}; - PeerTest::resetId(); - auto peer = makePeerTest(env, context_, protocolVersion_); + auto peer = makeCapturePeer(env); peer->onMessage(createRequest(numNodeIds)); // A request outside the accepted node-ID count is charged kFeeInvalidData; one inside // it is not. The JobQueue handler may run concurrently and update the fee in the // accepted case. BEAST_EXPECT( - expectRejected ? (peer->getCurrentFeeCharge() == resource::kFeeInvalidData) - : !(peer->getCurrentFeeCharge() == resource::kFeeInvalidData)); + expectRejected ? (peer->feeCharge() == resource::kFeeInvalidData) + : !(peer->feeCharge() == resource::kFeeInvalidData)); } void @@ -84,9 +87,8 @@ class TMGetLedger_test : public beast::unit_test::Suite Env env{*this}; env.close(); - PeerTest::resetId(); - auto peer = makePeerTest(env, context_, protocolVersion_); + auto peer = makeCapturePeer(env); // Ask for the account-state root node of the closed ledger. auto request = createRequest(numNodeIds); @@ -96,7 +98,7 @@ class TMGetLedger_test : public beast::unit_test::Suite peer->runProcessLedgerRequest(request, std::vector(numNodeIds)); - auto sentMessage = peer->getLastSentMessage(); + auto sentMessage = peer->lastSent(); BEAST_EXPECT(sentMessage != nullptr); if (!sentMessage) { diff --git a/src/test/overlay/TMGetObjectByHash_test.cpp b/src/test/overlay/TMGetObjectByHash_test.cpp index 84495fec37..1aafd77f2f 100644 --- a/src/test/overlay/TMGetObjectByHash_test.cpp +++ b/src/test/overlay/TMGetObjectByHash_test.cpp @@ -1,26 +1,17 @@ #include -#include +#include #include #include #include -#include -#include #include #include #include -#include #include #include #include -#include -#include -#include -#include -#include - #include #include @@ -41,8 +32,21 @@ using namespace jtx; */ class TMGetObjectByHash_test : public beast::unit_test::Suite { - PeerTest::SharedContext context_{makeSslContext("")}; - ProtocolVersion protocolVersion_{1, 7}; + /** + * Calls the JtLedgerReq-dispatched processor synchronously, so the reply is + * visible through `sent()`. + */ + class GetObjectPeer : public CapturePeer + { + public: + using CapturePeer::CapturePeer; + + void + runProcessGetObjectByHash(std::shared_ptr const& m) + { + processGetObjectByHash(m); + } + }; static std::shared_ptr createRequest(size_t const numObjects, Env& env) @@ -90,15 +94,13 @@ class TMGetObjectByHash_test : public beast::unit_test::Suite testcase("Reply Object Count"); Env env(*this); - PeerTest::resetId(); - - auto peer = makePeerTest(env, context_, protocolVersion_); + auto peer = makeCapturePeer(env); auto request = createRequest(numObjects, env); peer->runProcessGetObjectByHash(request); // Verify that a reply was sent - auto sentMessage = peer->getLastSentMessage(); + auto sentMessage = peer->lastSent(); BEAST_EXPECT(sentMessage != nullptr); // Parse the reply message diff --git a/src/test/overlay/TMTransaction_test.cpp b/src/test/overlay/TMTransaction_test.cpp index b208b3d81b..5a23e25005 100644 --- a/src/test/overlay/TMTransaction_test.cpp +++ b/src/test/overlay/TMTransaction_test.cpp @@ -1,21 +1,10 @@ #include #include -#include +#include -#include -#include -#include - -#include #include #include -#include -#include -#include -#include -#include - #include #include @@ -26,18 +15,14 @@ using namespace jtx; class TMTransaction_test : public beast::unit_test::Suite { - PeerTest::SharedContext context_{makeSslContext("")}; - ProtocolVersion protocolVersion_{1, 7}; - void testFailureDeserializingTransactionIsCharged() { testcase("Undeserializable Transaction Is Charged"); Env env{*this, envconfig()}; - PeerTest::resetId(); - auto peer = makePeerTest(env, context_, protocolVersion_); + auto peer = makeCapturePeer(env); auto tx = std::make_shared(); tx->set_status(protocol::tsNEW); @@ -45,7 +30,7 @@ class TMTransaction_test : public beast::unit_test::Suite tx->set_rawtransaction("\x01\x02\x03", 3); peer->onMessage(tx); - BEAST_EXPECT(peer->getCurrentFeeCharge() == resource::kFeeInvalidData); + BEAST_EXPECT(peer->feeCharge() == resource::kFeeInvalidData); } void diff --git a/src/test/overlay/TMTransactions_test.cpp b/src/test/overlay/TMTransactions_test.cpp index 67d36cc04b..87c09498f2 100644 --- a/src/test/overlay/TMTransactions_test.cpp +++ b/src/test/overlay/TMTransactions_test.cpp @@ -1,28 +1,21 @@ #include #include #include -#include +#include #include -#include -#include -#include -#include +#include -#include #include #include - -#include -#include -#include -#include -#include +#include #include #include #include +#include +#include namespace xrpl::test { @@ -30,9 +23,6 @@ using namespace jtx; class TMTransactions_test : public beast::unit_test::Suite { - PeerTest::SharedContext context_{makeSslContext("")}; - ProtocolVersion protocolVersion_{1, 7}; - static std::shared_ptr createRequest(std::size_t const numTransactions) { @@ -55,13 +45,17 @@ class TMTransactions_test : public beast::unit_test::Suite *this, envconfig(), std::make_unique(kLimitExceededMessage, &foundExpectedLog)}; - PeerTest::resetId(); - auto peer = makePeerTest(env, context_, protocolVersion_); - peer->txReduceRelayEnabled(true); + // `PeerImp` decides `txReduceRelayEnabled()` in its constructor, from + // the config and the handshake header, so set this first. + env.app().config().txReduceRelayEnable = true; + http_request_type request; + request.insert("X-Protocol-Ctl", makeFeaturesRequestHeader(false, false, true, false)); + + auto peer = makeCapturePeer(env, std::nullopt, std::move(request)); peer->onMessage(createRequest(numTransactions)); - auto fee = peer->getCurrentFeeCharge(); + auto fee = peer->feeCharge(); if (expectRejected) { BEAST_EXPECT(fee == resource::kFeeMalformedRequest); diff --git a/src/test/overlay/reduce_relay_test.cpp b/src/test/overlay/reduce_relay_test.cpp index 4091efa0ca..ff3eb51a4d 100644 --- a/src/test/overlay/reduce_relay_test.cpp +++ b/src/test/overlay/reduce_relay_test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -12,10 +13,8 @@ #include #include #include -#include #include #include -#include #include #include #include @@ -27,7 +26,6 @@ #include #include #include -#include #include #include #include @@ -67,16 +65,16 @@ static constexpr std::uint32_t kMaxMessages = 200000; /** * Simulate two entities - peer directly connected to the server * (via squelch in PeerSim) and PeerImp (via Overlay) + * + * `PeerStub` supplies the rest of the `Peer` interface as no-ops. */ -class PeerPartial : public Peer +class PeerPartial : public PeerStub { public: - PeerPartial() : nodePublicKey(derivePublicKey(KeyType::Ed25519, randomSecretKey())) - { - } + using PeerStub::PeerStub; + // Keep the base overload visible; the one below would otherwise hide it. + using PeerStub::send; - PublicKey nodePublicKey; - ~PeerPartial() override = default; virtual void onMessage(MessageSPtr const& m, SquelchCB f) = 0; virtual void @@ -86,111 +84,6 @@ public: { onMessage(squelch); } - - // dummy implementation - void - send(std::shared_ptr const& m) override - { - } - [[nodiscard]] beast::ip::Endpoint - getRemoteAddress() const override - { - return {}; - } - void - charge(resource::Charge const& fee, std::string const& context = {}) override - { - } - [[nodiscard]] bool - cluster() const override - { - return false; - } - [[nodiscard]] bool - isHighLatency() const override - { - return false; - } - [[nodiscard]] int - getScore(bool) const override - { - return 0; - } - [[nodiscard]] PublicKey const& - getNodePublic() const override - { - return nodePublicKey; - } - json::Value - json() override - { - return {}; - } - [[nodiscard]] bool - supportsFeature(ProtocolFeature f) const override - { - return false; - } - [[nodiscard]] std::optional - publisherListSequence(PublicKey const&) const override - { - return {}; - } - void - setPublisherListSequence(PublicKey const&, std::size_t const) override - { - } - [[nodiscard]] uint256 - getClosedLedgerHash() const override - { - static uint256 const kHash{}; - return kHash; - } - [[nodiscard]] bool - hasLedger(uint256 const& hash, std::uint32_t seq) const override - { - return false; - } - void - ledgerRange(std::uint32_t& minSeq, std::uint32_t& maxSeq) const override - { - } - [[nodiscard]] bool - hasTxSet(uint256 const& hash) const override - { - return false; - } - void - cycleStatus() override - { - } - bool - hasRange(std::uint32_t uMin, std::uint32_t uMax) override - { - return false; - } - [[nodiscard]] bool - compressionEnabled() const override - { - return false; - } - [[nodiscard]] bool - txReduceRelayEnabled() const override - { - return false; - } - void - sendTxQueue() override - { - } - void - addTxQueue(uint256 const&) override - { - } - void - removeTxQueue(uint256 const&) override - { - } }; /** @@ -466,24 +359,13 @@ class PeerSim : public PeerPartial, public std::enable_shared_from_this { public: using id_t = Peer::id_t; - PeerSim(Overlay& overlay, beast::Journal journal) : overlay_(overlay), squelch_(journal) + PeerSim(Overlay& overlay, beast::Journal journal) + : PeerPartial(sid++), overlay_(overlay), squelch_(journal) { } ~PeerSim() override = default; - id_t - id() const override - { - return id_; - } - - std::string const& - fingerprint() const override - { - return fingerprint_; - } - static void resetId() { @@ -525,8 +407,6 @@ public: private: inline static id_t sid = 0; - std::string fingerprint_; - id_t id_{sid++}; Overlay& overlay_; reduce_relay::Squelch squelch_; }; diff --git a/src/test/overlay/tx_reduce_relay_test.cpp b/src/test/overlay/tx_reduce_relay_test.cpp index 8626d3e19c..e97fba88e5 100644 --- a/src/test/overlay/tx_reduce_relay_test.cpp +++ b/src/test/overlay/tx_reduce_relay_test.cpp @@ -1,38 +1,24 @@ #include #include +#include #include #include -#include #include #include #include #include -#include #include -#include -#include #include -#include #include #include #include #include -#include #include -#include -#include -#include -#include -#include -#include -#include - #include -#include #include #include #include @@ -47,13 +33,6 @@ namespace xrpl::test { class tx_reduce_relay_test : public beast::unit_test::Suite { -public: - using socket_type = boost::asio::ip::tcp::socket; - using middle_type = boost::beast::tcp_stream; - using stream_type = boost::beast::ssl_stream; - using shared_context = std::shared_ptr; - -private: void doTest(std::string const& msg, bool log, std::function f) { @@ -116,107 +95,83 @@ private: }); } - class PeerTest : public PeerImp + /** + * Counts queued transaction hashes. Relayed messages are counted through + * the inherited `sent()`. + */ + class TxReducePeer : public CapturePeer { public: - PeerTest( - Application& app, - std::shared_ptr const& slot, - http_request_type&& request, - PublicKey const& publicKey, - ProtocolVersion protocol, - resource::Consumer consumer, - std::unique_ptr&& streamPtr, - OverlayImpl& overlay) - : PeerImp( - app, - sid, - slot, - std::move(request), - publicKey, - protocol, - consumer, - std::move(streamPtr), - overlay) - { - sid++; - } - ~PeerTest() override = default; + using CapturePeer::CapturePeer; void - run() override + addTxQueue(uint256 const&) override { + ++queued_; } - void - send(std::shared_ptr const&) override + + /** + * @return The number of transaction hashes queued for this peer. + */ + std::size_t + queued() const { - sendTx++; + return queued_; } - void - addTxQueue(uint256 const& hash) override - { - queueTx++; - } - static void - init() - { - queueTx = 0; - sendTx = 0; - sid = 0; - } - inline static std::size_t sid = 0; - inline static std::uint16_t queueTx = 0; - inline static std::uint16_t sendTx = 0; + + private: + std::size_t queued_{0}; }; - std::uint16_t lid_{0}; - std::uint16_t rid_{1}; - shared_context context_; - ProtocolVersion protocolVersion_; - boost::beast::multi_buffer readBuf_; - -public: - tx_reduce_relay_test() : context_(makeSslContext("")), protocolVersion_{1, 7} - { - } - -private: + /** + * Build one peer and register it with the overlay. + * + * The first `nDisabled` peers get no `X-Protocol-Ctl` header, which leaves + * tx reduce-relay disabled on them. Built first, they sit at the front of + * `peers`, where `testRelay`'s skip set expects them. + * + * @param env The environment owning the overlay. + * @param peers Receives the peer; the overlay holds only a weak + * pointer, so the caller keeps it alive. + * @param nDisabled How many more peers to leave disabled; decremented + * per peer built. + */ void - addPeer(jtx::Env& env, std::vector>& peers, std::uint16_t& nDisabled) + addPeer( + jtx::Env& env, + std::vector>& peers, + std::uint16_t& nDisabled) { auto& overlay = dynamic_cast(env.app().getOverlay()); - boost::beast::http::request request; - (nDisabled == 0) - ? request.insert("X-Protocol-Ctl", makeFeaturesRequestHeader(false, false, true, false)) - : (void)nDisabled--; - auto streamPtr = std::make_unique( - socket_type(std::forward(env.app().getIOContext())), - *context_); - beast::ip::Endpoint const local( - boost::asio::ip::make_address("172.1.1." + std::to_string(lid_))); - beast::ip::Endpoint const remote( - boost::asio::ip::make_address("172.1.1." + std::to_string(rid_))); PublicKey const key(std::get<0>(randomKeyPair(KeyType::Ed25519))); - auto consumer = overlay.resourceManager().newInboundEndpoint(remote); - auto [slot, _] = overlay.peerFinder().newInboundSlot(local, remote); - auto const peer = std::make_shared( - env.app(), - slot, - std::move(request), - key, - protocolVersion_, - consumer, - std::move(streamPtr), - overlay); + + bool const disabled = nDisabled > 0; + if (disabled) + --nDisabled; + + http_request_type request; + if (!disabled) + request.insert("X-Protocol-Ctl", makeFeaturesRequestHeader(false, false, true, false)); + BEAST_EXPECT(overlay.findPeerByPublicKey(key) == std::shared_ptr{}); - overlay.addActive(peer); + auto const peer = makeCapturePeer(env, key, std::move(request)); BEAST_EXPECT(overlay.findPeerByPublicKey(key) == peer); - peers.emplace_back(peer); // overlay stores week ptr to PeerImp - lid_ += 2; - rid_ += 2; - assert(lid_ <= 254); + peers.emplace_back(peer); } + /** + * Relay one transaction to `nPeers` peers and check the split. + * + * @param test The testcase name. + * @param txRREnabled The `tx_enable` config value. + * @param nPeers How many peers to attach to the overlay. + * @param nDisabled How many of those peers have reduce-relay disabled. + * @param minPeers The `tx_min_peers` config value. + * @param relayPercentage The `tx_relay_percentage` config value. + * @param expectRelay The expected number of peers relayed to. + * @param expectQueue The expected number of peers queued for. + * @param nSkip How many of the first-built peers to skip. + */ void testRelay( std::string const& test, @@ -227,20 +182,30 @@ private: std::uint16_t relayPercentage, std::uint16_t expectRelay, std::uint16_t expectQueue, - std::set const& toSkip = {}) + std::size_t nSkip = 0) { testcase(test); jtx::Env env(*this); - std::vector> peers; + std::vector> peers; + // `PeerImp` decides `txReduceRelayEnabled()` in its constructor, from + // the config and the handshake header, so set these first. env.app().config().txReduceRelayEnable = txRREnabled; env.app().config().txReduceRelayMinPeers = minPeers; env.app().config().txRelayPercentage = relayPercentage; - PeerTest::init(); - lid_ = 0; - rid_ = 0; for (int i = 0; i < nPeers; i++) addPeer(env, peers, nDisabled); + // An under-filled skip set would also fail the relay counts below, for + // a reason that looks unrelated. + if (!BEAST_EXPECT(nSkip <= peers.size())) + return; + + // Skip the peers built first, so the skip set overlaps the disabled + // peers as the expected counts assume. + std::set toSkip; + for (std::size_t i = 0; i < nSkip; ++i) + toSkip.insert(peers[i]->id()); + auto const jtx = env.jt(noop(env.master)); if (BEAST_EXPECT(jtx.stx)) { @@ -251,7 +216,15 @@ private: m.set_deferred(false); m.set_status(protocol::TransactionStatus::tsNEW); env.app().getOverlay().relay(uint256{0}, m, toSkip); - BEAST_EXPECT(PeerTest::sendTx == expectRelay && PeerTest::queueTx == expectQueue); + + std::size_t sendTx = 0; + std::size_t queueTx = 0; + for (auto const& peer : peers) + { + sendTx += peer->sent().size(); + queueTx += peer->queued(); + } + BEAST_EXPECT(sendTx == expectRelay && queueTx == expectQueue); } } @@ -259,12 +232,11 @@ private: run() override { bool const log = false; - std::set skip = {0, 1, 2, 3, 4}; testConfig(log); // relay to all peers, no hash queue testRelay("feature disabled", false, 10, 0, 10, 25, 10, 0); // relay to nPeers - skip (10-5=5) - testRelay("feature disabled & skip", false, 10, 0, 10, 25, 5, 0, skip); + testRelay("feature disabled & skip", false, 10, 0, 10, 25, 5, 0, 5); // relay to all peers because min is greater than nPeers testRelay("relay all 1", true, 10, 0, 20, 25, 10, 0); // relay to all peers because min + disabled is greater thant nPeers @@ -275,24 +247,22 @@ private: // relay to minPeers + 25% of (nPeers - nPeers) - skip // (20+0.25*(60-20)-5=25), queue the rest, skip counts towards relayed // (60-25-5=30) - testRelay("skip", true, 60, 0, 20, 25, 25, 30, skip); + testRelay("skip", true, 60, 0, 20, 25, 25, 30, 5); // relay to minPeers + disabled + 25% of (nPeers - minPeers - disabled) // (20+10+0.25*(70-20-10)=40), queue the rest (30) testRelay("disabled", true, 70, 10, 20, 25, 40, 30); // relay to minPeers + disabled-not-in-skip + 25% of (nPeers - minPeers // - disabled) (20+5+0.25*(70-20-10)=35), queue the rest, skip counts // towards relayed (70-35-5=30)) - testRelay("disabled & skip", true, 70, 10, 20, 25, 35, 30, skip); + testRelay("disabled & skip", true, 70, 10, 20, 25, 35, 30, 5); // relay to minPeers + disabled + 25% of (nPeers - minPeers - disabled) // - skip (10+5+0.25*(15-10-5)-10=5), queue the rest, skip counts // towards relayed (15-5-10=0) - skip = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - testRelay("disabled & skip, no queue", true, 15, 5, 10, 25, 5, 0, skip); + testRelay("disabled & skip, no queue", true, 15, 5, 10, 25, 5, 0, 10); // relay to minPeers + disabled + 25% of (nPeers - minPeers - disabled) // - skip (10+2+0.25*(20-10-2)-14=0), queue the rest, skip counts // towards relayed (20-14=6) - skip = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; - testRelay("disabled & skip, no relay", true, 20, 2, 10, 25, 0, 6, skip); + testRelay("disabled & skip, no relay", true, 20, 2, 10, 25, 0, 6, 14); } }; diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index e7c5dd4a80..deb19e3a3f 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -13,17 +13,21 @@ #include #include +#include #include #include #include +#include #include #include #include +#include #include #include #include +#include #include #include #include @@ -807,6 +811,95 @@ class LedgerRPC_test : public beast::unit_test::Suite } } + void + testLedgerExpandedTransactionsCTID() + { + testcase("Expanded Transactions CTID"); + using namespace test::jtx; + + Env env{*this}; + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + uint32_t const netID = env.app().getNetworkIDService().getNetworkID(); + + // API v2 non-binary: CTID present + { + json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::transactions] = true; + jvParams[jss::expand] = true; + jvParams[jss::api_version] = 2; + auto const jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::status] == "success"); + auto const& txns = jrr[jss::ledger][jss::transactions]; + BEAST_EXPECT(txns.isArray() && txns.size() > 0); + for (auto const& txn : txns) + { + BEAST_EXPECT(txn.isMember(jss::ctid)); + auto const expectedCtid = rpc::encodeCTID( + jrr[jss::ledger][jss::ledger_index].asUInt(), + txn[jss::meta][sfTransactionIndex.jsonName].asUInt(), + netID); + // NOLINTBEGIN(bugprone-unchecked-optional-access) + if (BEAST_EXPECT(expectedCtid.has_value())) + BEAST_EXPECT(txn[jss::ctid] == expectedCtid.value()); + // NOLINTEND(bugprone-unchecked-optional-access) + } + } + + // API v1 non-binary: CTID present + { + json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::transactions] = true; + jvParams[jss::expand] = true; + auto const jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::status] == "success"); + auto const& txns = jrr[jss::ledger][jss::transactions]; + BEAST_EXPECT(txns.isArray() && txns.size() > 0); + for (auto const& txn : txns) + { + BEAST_EXPECT(txn.isMember(jss::ctid)); + } + } + + // Binary expanded: CTID present + { + json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::transactions] = true; + jvParams[jss::expand] = true; + jvParams[jss::binary] = true; + jvParams[jss::api_version] = 2; + auto const jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::status] == "success"); + auto const& txns = jrr[jss::ledger][jss::transactions]; + BEAST_EXPECT(txns.isArray() && txns.size() > 0); + for (auto const& txn : txns) + { + BEAST_EXPECT(txn.isMember(jss::ctid)); + } + } + + // Non-expanded: transactions are plain hash strings, no CTID + { + json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::transactions] = true; + jvParams[jss::api_version] = 2; + auto const jrr = env.rpc("json", "ledger", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::status] == "success"); + auto const& txns = jrr[jss::ledger][jss::transactions]; + BEAST_EXPECT(txns.isArray() && txns.size() > 0); + for (auto const& txn : txns) + { + BEAST_EXPECT(txn.isString()); + } + } + } + public: void run() override @@ -822,6 +915,7 @@ public: testNoQueue(); testQueue(); testLedgerAccountsOption(); + testLedgerExpandedTransactionsCTID(); } }; diff --git a/src/tests/libxrpl/basics/MallocTrim.cpp b/src/tests/libxrpl/basics/MallocTrim.cpp index 52151262b0..0f98a156a5 100644 --- a/src/tests/libxrpl/basics/MallocTrim.cpp +++ b/src/tests/libxrpl/basics/MallocTrim.cpp @@ -49,7 +49,7 @@ TEST(MallocTrimReport, structure) } #if defined(__GLIBC__) && BOOST_OS_LINUX -TEST(parseStatmRSSkB, standard_format) +TEST(ParseStatmRSSkB, standard_format) { using xrpl::detail::parseStatmRSSkB; @@ -121,7 +121,7 @@ TEST(parseStatmRSSkB, standard_format) } #endif -TEST(mallocTrim, without_debug_logging) +TEST(MallocTrim, without_debug_logging) { beast::Journal const journal{beast::Journal::getNullSink()}; @@ -144,7 +144,7 @@ TEST(mallocTrim, without_debug_logging) #endif } -TEST(mallocTrim, empty_tag) +TEST(MallocTrim, empty_tag) { beast::Journal const journal{beast::Journal::getNullSink()}; MallocTrimReport const report = mallocTrim("", journal); @@ -157,7 +157,7 @@ TEST(mallocTrim, empty_tag) #endif } -TEST(mallocTrim, with_debug_logging) +TEST(MallocTrim, with_debug_logging) { struct DebugSink : public beast::Journal::Sink { @@ -194,7 +194,7 @@ TEST(mallocTrim, with_debug_logging) #endif } -TEST(mallocTrim, repeated_calls) +TEST(MallocTrim, repeated_calls) { beast::Journal const journal{beast::Journal::getNullSink()}; diff --git a/src/tests/libxrpl/basics/RangeSet.cpp b/src/tests/libxrpl/basics/RangeSet.cpp index 44b13ad581..2e224c79f7 100644 --- a/src/tests/libxrpl/basics/RangeSet.cpp +++ b/src/tests/libxrpl/basics/RangeSet.cpp @@ -10,7 +10,7 @@ using namespace xrpl; -TEST(RangeSet, prevMissing) +TEST(RangeSet, prev_missing) { // Set will include: // [ 0, 5] @@ -36,7 +36,7 @@ TEST(RangeSet, prevMissing) } } -TEST(RangeSet, toString) +TEST(RangeSet, to_string) { RangeSet set; EXPECT_EQ(to_string(set), "empty"); @@ -54,7 +54,7 @@ TEST(RangeSet, toString) EXPECT_EQ(to_string(set), "1-2,6"); } -TEST(RangeSet, fromString) +TEST(RangeSet, from_string) { RangeSet set; diff --git a/src/tests/libxrpl/basics/StringUtilities.cpp b/src/tests/libxrpl/basics/StringUtilities.cpp index 0180e25db0..1859de2cc1 100644 --- a/src/tests/libxrpl/basics/StringUtilities.cpp +++ b/src/tests/libxrpl/basics/StringUtilities.cpp @@ -290,7 +290,7 @@ TEST_F(StringUtilitiesTest, to_string) EXPECT_EQ(result, "hello"); } -TEST_F(StringUtilitiesTest, trimWhitespace) +TEST_F(StringUtilitiesTest, trim_whitespace) { EXPECT_EQ(trimWhitespace(""), ""); EXPECT_EQ(trimWhitespace(" "), ""); @@ -303,7 +303,7 @@ TEST_F(StringUtilitiesTest, trimWhitespace) EXPECT_EQ(trimWhitespace(" a b\tc "), "a b\tc"); } -TEST_F(StringUtilitiesTest, toLower) +TEST_F(StringUtilitiesTest, to_lower) { EXPECT_EQ(toLower(""), ""); EXPECT_EQ(toLower("ABC"), "abc"); @@ -318,7 +318,7 @@ TEST_F(StringUtilitiesTest, toLower) // Both helpers are documented as depending only on their input. Guard that by // checking the bytes just outside ASCII, which a locale-aware isspace/tolower // could classify differently. -TEST_F(StringUtilitiesTest, trimAndLowerIgnoreLocale) +TEST_F(StringUtilitiesTest, trim_and_lower_ignore_locale) { // 0xA0 is NO-BREAK SPACE in Latin-1 and is whitespace to some locales. std::string const nbsp("\xA0", 1); diff --git a/src/tests/libxrpl/basics/base64.cpp b/src/tests/libxrpl/basics/base64.cpp index d26d23700a..c9f8331c98 100644 --- a/src/tests/libxrpl/basics/base64.cpp +++ b/src/tests/libxrpl/basics/base64.cpp @@ -14,7 +14,7 @@ check(std::string const& in, std::string const& out) EXPECT_EQ(base64Decode(encoded), in); } -TEST(base64, base64) +TEST(Base64, base64) { // cspell: disable check("", ""); diff --git a/src/tests/libxrpl/basics/base_uint.cpp b/src/tests/libxrpl/basics/base_uint.cpp index 969705b5b7..4783820205 100644 --- a/src/tests/libxrpl/basics/base_uint.cpp +++ b/src/tests/libxrpl/basics/base_uint.cpp @@ -128,7 +128,7 @@ struct BaseUintTest : public ::testing::Test using BaseUintDeathTest = BaseUintTest; -TEST_F(BaseUintDeathTest, fromRaw_size_mismatch) +TEST_F(BaseUintDeathTest, from_raw_size_mismatch) { // ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts. Rather than twist // these tests into knots to make them work, just skip them. diff --git a/src/tests/libxrpl/basics/contract.cpp b/src/tests/libxrpl/basics/contract.cpp index 0c6b32a7ad..e9404da78b 100644 --- a/src/tests/libxrpl/basics/contract.cpp +++ b/src/tests/libxrpl/basics/contract.cpp @@ -6,7 +6,7 @@ using namespace xrpl; -TEST(contract, contract) +TEST(Contract, contract) { try { diff --git a/src/tests/libxrpl/basics/mulDiv.cpp b/src/tests/libxrpl/basics/mulDiv.cpp index 725bef399e..cdc672a444 100644 --- a/src/tests/libxrpl/basics/mulDiv.cpp +++ b/src/tests/libxrpl/basics/mulDiv.cpp @@ -7,7 +7,7 @@ using namespace xrpl; -TEST(mulDiv, mulDiv) +TEST(MulDiv, mul_div) { auto const max = std::numeric_limits::max(); std::uint64_t const max32 = std::numeric_limits::max(); diff --git a/src/tests/libxrpl/basics/scope.cpp b/src/tests/libxrpl/basics/scope.cpp index dc5623e967..71186d3d90 100644 --- a/src/tests/libxrpl/basics/scope.cpp +++ b/src/tests/libxrpl/basics/scope.cpp @@ -6,7 +6,7 @@ using namespace xrpl; -TEST(scope, ScopeExit) +TEST(Scope, scope_exit) { // ScopeExit always executes the functor on destruction, // unless release() is called @@ -56,7 +56,7 @@ TEST(scope, ScopeExit) EXPECT_EQ(i, 5); } -TEST(scope, ScopeFail) +TEST(Scope, scope_fail) { // ScopeFail executes the functor on destruction only // if an exception is unwinding, unless release() is called @@ -106,7 +106,7 @@ TEST(scope, ScopeFail) EXPECT_EQ(i, 5); } -TEST(scope, ScopeSuccess) +TEST(Scope, scope_success) { // ScopeSuccess executes the functor on destruction only // if an exception is not unwinding, unless release() is called diff --git a/src/tests/libxrpl/basics/tagged_integer.cpp b/src/tests/libxrpl/basics/tagged_integer.cpp index 7382527d4d..dc553d4c0f 100644 --- a/src/tests/libxrpl/basics/tagged_integer.cpp +++ b/src/tests/libxrpl/basics/tagged_integer.cpp @@ -105,7 +105,7 @@ static_assert( using TagInt = TaggedInteger; -TEST(tagged_integer, comparison_operators) +TEST(TaggedInteger, comparison_operators) { TagInt const zero(0); TagInt const one(1); @@ -131,7 +131,7 @@ TEST(tagged_integer, comparison_operators) EXPECT_FALSE(one <= zero); } -TEST(tagged_integer, increment_decrement_operators) +TEST(TaggedInteger, increment_decrement_operators) { TagInt const zero(0); TagInt const one(1); @@ -146,7 +146,7 @@ TEST(tagged_integer, increment_decrement_operators) EXPECT_EQ(a, zero); } -TEST(tagged_integer, arithmetic_operators) +TEST(TaggedInteger, arithmetic_operators) { TagInt const a{-2}; EXPECT_EQ(+a, TagInt{-2}); @@ -166,7 +166,7 @@ TEST(tagged_integer, arithmetic_operators) EXPECT_EQ((TagInt{16} >> TagInt{2}), TagInt{4}); } -TEST(tagged_integer, assignment_operators) +TEST(TaggedInteger, assignment_operators) { TagInt a{-2}; TagInt b{0}; diff --git a/src/tests/libxrpl/crypto/csprng.cpp b/src/tests/libxrpl/crypto/csprng.cpp index 957f7f5c56..71e20f2ddc 100644 --- a/src/tests/libxrpl/crypto/csprng.cpp +++ b/src/tests/libxrpl/crypto/csprng.cpp @@ -6,7 +6,7 @@ using namespace xrpl; -TEST(csprng, get_values) +TEST(Csprng, get_values) { auto& engine = cryptoPrng(); auto randVal = engine(); diff --git a/src/tests/libxrpl/json/Value.cpp b/src/tests/libxrpl/json/Value.cpp index a58a5df9fd..1aa0756419 100644 --- a/src/tests/libxrpl/json/Value.cpp +++ b/src/tests/libxrpl/json/Value.cpp @@ -21,7 +21,7 @@ namespace xrpl { -TEST(json_value, limits) +TEST(JsonValue, limits) { using namespace json; static_assert(Value::kMinInt == Int(~(UInt(-1) / 2))); @@ -29,7 +29,7 @@ TEST(json_value, limits) static_assert(Value::kMaxUInt == UInt(-1)); } -TEST(json_value, construct_and_compare_Json_StaticString) +TEST(JsonValue, construct_and_compare_json_static_string) { static constexpr char kSample[]{"Contents of a json::StaticString"}; @@ -52,7 +52,7 @@ TEST(json_value, construct_and_compare_Json_StaticString) EXPECT_NE(kTest3, str); } -TEST(json_value, different_types) +TEST(JsonValue, different_types) { // Exercise ValueType constructor static constexpr json::StaticString kStaticStr{"staticStr"}; @@ -206,7 +206,7 @@ TEST(json_value, different_types) } } -TEST(json_value, compare_strings) +TEST(JsonValue, compare_strings) { auto doCompare = [&](json::Value const& lhs, json::Value const& rhs, @@ -560,7 +560,7 @@ TEST(json_value, compare_strings) #pragma pop_macro("DO_COMPARE") } -TEST(json_value, bool) +TEST(JsonValue, bool) { EXPECT_FALSE(json::Value()); @@ -583,7 +583,7 @@ TEST(json_value, bool) EXPECT_TRUE(bool(object)); } -TEST(json_value, bad_json) +TEST(JsonValue, bad_json) { char const* s(R"({"method":"ledger","params":[{"ledger_index":1e300}]})"); @@ -607,7 +607,7 @@ parseValue(std::string const& doc) } // namespace -TEST(json_value, parse_double_valid) +TEST(JsonValue, parse_double_valid) { // 1e300 is large but still representable, so it parses (unlike the out-of-range cases below). for (auto const& [text, expected] : @@ -627,14 +627,14 @@ TEST(json_value, parse_double_valid) } } -TEST(json_value, parse_double_out_of_range) +TEST(JsonValue, parse_double_out_of_range) { // Magnitudes with no finite double representation are rejected. for (char const* oor : {"1e400", "-1e400", "0.001e500", "1e-400", "-1e-400", "123e-500"}) EXPECT_FALSE(parseValue(oor).has_value()) << oor; } -TEST(json_value, parse_double_malformed) +TEST(JsonValue, parse_double_malformed) { // readNumber() collects any run of digits and '.eE+-' into a single Double // token, so these malformed tokens reach decodeDouble. Each has a valid @@ -644,7 +644,7 @@ TEST(json_value, parse_double_malformed) EXPECT_FALSE(parseValue(bad).has_value()) << bad; } -TEST(json_value, edge_cases) +TEST(JsonValue, edge_cases) { std::uint32_t const maxUInt = std::numeric_limits::max(); std::int32_t const maxInt = std::numeric_limits::max(); @@ -791,7 +791,7 @@ TEST(json_value, edge_cases) } } -TEST(json_value, copy) +TEST(JsonValue, copy) { json::Value v1{2.5}; EXPECT_TRUE(v1.isDouble()); @@ -812,7 +812,7 @@ TEST(json_value, copy) EXPECT_EQ(v1, v2); } -TEST(json_value, move) +TEST(JsonValue, move) { json::Value v1{2.5}; EXPECT_TRUE(v1.isDouble()); @@ -831,7 +831,7 @@ TEST(json_value, move) EXPECT_NE(v1, v2); // NOLINT(bugprone-use-after-move) } -TEST(json_value, comparisons) +TEST(JsonValue, comparisons) { json::Value a, b; auto testEquals = [&](std::string const& name) { @@ -886,7 +886,7 @@ TEST(json_value, comparisons) testGreaterThan("big"); } -TEST(json_value, compact) +TEST(JsonValue, compact) { json::Value j; json::Reader r; @@ -909,7 +909,7 @@ TEST(json_value, compact) } } -TEST(json_value, conversions) +TEST(JsonValue, conversions) { // We have json::ValueType::Real but json::Value::asDouble. // TODO: What's the thinking here? @@ -1125,7 +1125,7 @@ TEST(json_value, conversions) } } -TEST(json_value, access_members) +TEST(JsonValue, access_members) { json::Value val; EXPECT_EQ(val.type(), json::ValueType::Null); @@ -1218,7 +1218,7 @@ TEST(json_value, access_members) } } -TEST(json_value, remove_members) +TEST(JsonValue, remove_members) { json::Value val; EXPECT_EQ(val.removeMember(std::string("member")).type(), json::ValueType::Null); @@ -1245,7 +1245,7 @@ TEST(json_value, remove_members) EXPECT_EQ(val.size(), 0); } -TEST(json_value, iterator) +TEST(JsonValue, iterator) { { // Iterating an array. @@ -1331,7 +1331,7 @@ TEST(json_value, iterator) } } -TEST(json_value, nest_limits) +TEST(JsonValue, nest_limits) { json::Reader r; { @@ -1377,7 +1377,7 @@ TEST(json_value, nest_limits) } } -TEST(json_value, memory_leak) +TEST(JsonValue, memory_leak) { // When run with the address sanitizer, this test confirms there is no // memory leak with the scenarios below. diff --git a/src/tests/libxrpl/ledger/AMMEntry.cpp b/src/tests/libxrpl/ledger/AMMEntry.cpp index 6189d2b3e6..6013d0f38f 100644 --- a/src/tests/libxrpl/ledger/AMMEntry.cpp +++ b/src/tests/libxrpl/ledger/AMMEntry.cpp @@ -10,7 +10,7 @@ namespace xrpl::test { -TEST(AMMEntryTests, Constructors) +TEST(AMMEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/AccountRootEntry.cpp b/src/tests/libxrpl/ledger/AccountRootEntry.cpp index 964d0f2f43..4c8f18337e 100644 --- a/src/tests/libxrpl/ledger/AccountRootEntry.cpp +++ b/src/tests/libxrpl/ledger/AccountRootEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(AccountRootEntryTests, Constructors) +TEST(AccountRootEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/AmendmentsEntry.cpp b/src/tests/libxrpl/ledger/AmendmentsEntry.cpp index ec45b291ec..3f296759f3 100644 --- a/src/tests/libxrpl/ledger/AmendmentsEntry.cpp +++ b/src/tests/libxrpl/ledger/AmendmentsEntry.cpp @@ -7,7 +7,7 @@ namespace xrpl::test { -TEST(AmendmentsEntryTests, Constructors) +TEST(AmendmentsEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/BridgeEntry.cpp b/src/tests/libxrpl/ledger/BridgeEntry.cpp index bae65ea8b5..6e6a4394f8 100644 --- a/src/tests/libxrpl/ledger/BridgeEntry.cpp +++ b/src/tests/libxrpl/ledger/BridgeEntry.cpp @@ -11,7 +11,7 @@ namespace xrpl::test { -TEST(BridgeEntryTests, Constructors) +TEST(BridgeEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/CheckEntry.cpp b/src/tests/libxrpl/ledger/CheckEntry.cpp index da51da047e..58f6250b20 100644 --- a/src/tests/libxrpl/ledger/CheckEntry.cpp +++ b/src/tests/libxrpl/ledger/CheckEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(CheckEntryTests, Constructors) +TEST(CheckEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/CredentialEntry.cpp b/src/tests/libxrpl/ledger/CredentialEntry.cpp index ce3f80dca0..16b877858b 100644 --- a/src/tests/libxrpl/ledger/CredentialEntry.cpp +++ b/src/tests/libxrpl/ledger/CredentialEntry.cpp @@ -11,7 +11,7 @@ namespace xrpl::test { -TEST(CredentialEntryTests, Constructors) +TEST(CredentialEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/DIDEntry.cpp b/src/tests/libxrpl/ledger/DIDEntry.cpp index 41b27a486c..ff0017caad 100644 --- a/src/tests/libxrpl/ledger/DIDEntry.cpp +++ b/src/tests/libxrpl/ledger/DIDEntry.cpp @@ -7,7 +7,7 @@ namespace xrpl::test { -TEST(DIDEntryTests, Constructors) +TEST(DIDEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/DelegateEntry.cpp b/src/tests/libxrpl/ledger/DelegateEntry.cpp index a27299df46..3ffc3c73ed 100644 --- a/src/tests/libxrpl/ledger/DelegateEntry.cpp +++ b/src/tests/libxrpl/ledger/DelegateEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(DelegateEntryTests, Constructors) +TEST(DelegateEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/DepositPreauthEntry.cpp b/src/tests/libxrpl/ledger/DepositPreauthEntry.cpp index bba2a58c8a..115baa741d 100644 --- a/src/tests/libxrpl/ledger/DepositPreauthEntry.cpp +++ b/src/tests/libxrpl/ledger/DepositPreauthEntry.cpp @@ -14,7 +14,7 @@ namespace xrpl::test { -TEST(DepositPreauthEntryTests, Constructors) +TEST(DepositPreauthEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/DirectoryNodeEntry.cpp b/src/tests/libxrpl/ledger/DirectoryNodeEntry.cpp index 0efcb365f6..41349ea427 100644 --- a/src/tests/libxrpl/ledger/DirectoryNodeEntry.cpp +++ b/src/tests/libxrpl/ledger/DirectoryNodeEntry.cpp @@ -10,7 +10,7 @@ namespace xrpl::test { -TEST(DirectoryNodeEntryTests, Constructors) +TEST(DirectoryNodeEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/EscrowEntry.cpp b/src/tests/libxrpl/ledger/EscrowEntry.cpp index 35ee0d4ad5..2430589641 100644 --- a/src/tests/libxrpl/ledger/EscrowEntry.cpp +++ b/src/tests/libxrpl/ledger/EscrowEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(EscrowEntryTests, Constructors) +TEST(EscrowEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/FeeSettingsEntry.cpp b/src/tests/libxrpl/ledger/FeeSettingsEntry.cpp index 2528cdbca5..b7b8325736 100644 --- a/src/tests/libxrpl/ledger/FeeSettingsEntry.cpp +++ b/src/tests/libxrpl/ledger/FeeSettingsEntry.cpp @@ -7,7 +7,7 @@ namespace xrpl::test { -TEST(FeeSettingsEntryTests, Constructors) +TEST(FeeSettingsEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/LedgerHashesEntry.cpp b/src/tests/libxrpl/ledger/LedgerHashesEntry.cpp index e5c635c92c..94cb6b1db6 100644 --- a/src/tests/libxrpl/ledger/LedgerHashesEntry.cpp +++ b/src/tests/libxrpl/ledger/LedgerHashesEntry.cpp @@ -7,7 +7,7 @@ namespace xrpl::test { -TEST(LedgerHashesEntryTests, Constructors) +TEST(LedgerHashesEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/LoanBrokerEntry.cpp b/src/tests/libxrpl/ledger/LoanBrokerEntry.cpp index e3a180c21b..ab0ced00c0 100644 --- a/src/tests/libxrpl/ledger/LoanBrokerEntry.cpp +++ b/src/tests/libxrpl/ledger/LoanBrokerEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(LoanBrokerEntryTests, Constructors) +TEST(LoanBrokerEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/LoanEntry.cpp b/src/tests/libxrpl/ledger/LoanEntry.cpp index c91063c73f..23bc03bc0c 100644 --- a/src/tests/libxrpl/ledger/LoanEntry.cpp +++ b/src/tests/libxrpl/ledger/LoanEntry.cpp @@ -9,7 +9,7 @@ namespace xrpl::test { -TEST(LoanEntryTests, Constructors) +TEST(LoanEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/MPTokenEntry.cpp b/src/tests/libxrpl/ledger/MPTokenEntry.cpp index f93b210c87..fe89555c7e 100644 --- a/src/tests/libxrpl/ledger/MPTokenEntry.cpp +++ b/src/tests/libxrpl/ledger/MPTokenEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(MPTokenEntryTests, Constructors) +TEST(MPTokenEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/MPTokenIssuanceEntry.cpp b/src/tests/libxrpl/ledger/MPTokenIssuanceEntry.cpp index 2553f29435..825f75debe 100644 --- a/src/tests/libxrpl/ledger/MPTokenIssuanceEntry.cpp +++ b/src/tests/libxrpl/ledger/MPTokenIssuanceEntry.cpp @@ -10,7 +10,7 @@ namespace xrpl::test { -TEST(MPTokenIssuanceEntryTests, Constructors) +TEST(MPTokenIssuanceEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/NFTokenOfferEntry.cpp b/src/tests/libxrpl/ledger/NFTokenOfferEntry.cpp index 728977cab4..2ad676e220 100644 --- a/src/tests/libxrpl/ledger/NFTokenOfferEntry.cpp +++ b/src/tests/libxrpl/ledger/NFTokenOfferEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(NFTokenOfferEntryTests, Constructors) +TEST(NFTokenOfferEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/NFTokenPageEntry.cpp b/src/tests/libxrpl/ledger/NFTokenPageEntry.cpp index e5a5a1d7fd..e392cc52af 100644 --- a/src/tests/libxrpl/ledger/NFTokenPageEntry.cpp +++ b/src/tests/libxrpl/ledger/NFTokenPageEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(NFTokenPageEntryTests, Constructors) +TEST(NFTokenPageEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/NegativeUNLEntry.cpp b/src/tests/libxrpl/ledger/NegativeUNLEntry.cpp index 2a4bc0b59b..6a15578b3e 100644 --- a/src/tests/libxrpl/ledger/NegativeUNLEntry.cpp +++ b/src/tests/libxrpl/ledger/NegativeUNLEntry.cpp @@ -7,7 +7,7 @@ namespace xrpl::test { -TEST(NegativeUNLEntryTests, Constructors) +TEST(NegativeUNLEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/OfferEntry.cpp b/src/tests/libxrpl/ledger/OfferEntry.cpp index dc32679138..17f98b69c2 100644 --- a/src/tests/libxrpl/ledger/OfferEntry.cpp +++ b/src/tests/libxrpl/ledger/OfferEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(OfferEntryTests, Constructors) +TEST(OfferEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/OracleEntry.cpp b/src/tests/libxrpl/ledger/OracleEntry.cpp index a505017e1f..f467056c1e 100644 --- a/src/tests/libxrpl/ledger/OracleEntry.cpp +++ b/src/tests/libxrpl/ledger/OracleEntry.cpp @@ -9,7 +9,7 @@ namespace xrpl::test { -TEST(OracleEntryTests, Constructors) +TEST(OracleEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/PayChannelEntry.cpp b/src/tests/libxrpl/ledger/PayChannelEntry.cpp index 7866f34be0..82dcdb3be8 100644 --- a/src/tests/libxrpl/ledger/PayChannelEntry.cpp +++ b/src/tests/libxrpl/ledger/PayChannelEntry.cpp @@ -9,7 +9,7 @@ namespace xrpl::test { -TEST(PayChannelEntryTests, Constructors) +TEST(PayChannelEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/PermissionedDomainEntry.cpp b/src/tests/libxrpl/ledger/PermissionedDomainEntry.cpp index cbaa2f0300..feb3002c4e 100644 --- a/src/tests/libxrpl/ledger/PermissionedDomainEntry.cpp +++ b/src/tests/libxrpl/ledger/PermissionedDomainEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(PermissionedDomainEntryTests, Constructors) +TEST(PermissionedDomainEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/RippleStateEntry.cpp b/src/tests/libxrpl/ledger/RippleStateEntry.cpp index c46921bced..721dc09157 100644 --- a/src/tests/libxrpl/ledger/RippleStateEntry.cpp +++ b/src/tests/libxrpl/ledger/RippleStateEntry.cpp @@ -10,7 +10,7 @@ namespace xrpl::test { -TEST(RippleStateEntryTests, Constructors) +TEST(RippleStateEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/SLEBase.cpp b/src/tests/libxrpl/ledger/SLEBase.cpp index 910cb27f09..f721ea760d 100644 --- a/src/tests/libxrpl/ledger/SLEBase.cpp +++ b/src/tests/libxrpl/ledger/SLEBase.cpp @@ -149,7 +149,7 @@ protected: } }; -TEST_F(SLEBaseTests, ReadOnly) +TEST_F(SLEBaseTests, read_only) { AccountRootEntryR const absent(bob_.id(), env_.getClosedLedger()); EXPECT_FALSE(absent.exists()); @@ -168,7 +168,7 @@ TEST_F(SLEBaseTests, ReadOnly) EXPECT_EQ(&present.readView(), &env_.getClosedLedger()); } -TEST_F(SLEBaseTests, AdoptSLE) +TEST_F(SLEBaseTests, adopt_sle) { auto const sle = env_.getClosedLedger().read(keylet::account(alice_.id())); ASSERT_NE(sle, nullptr); @@ -201,7 +201,7 @@ TEST_F(SLEBaseTests, AdoptSLE) "writable entries must not be constructible from a bare SLE"); } -TEST_F(SLEBaseTests, WritableAccessors) +TEST_F(SLEBaseTests, writable_accessors) { ApplyViewImpl av(&env_.getClosedLedger(), TapNone); beast::Journal const j{beast::Journal::getNullSink()}; @@ -236,7 +236,7 @@ TEST_F(SLEBaseTests, WritableAccessors) !HasApplyView, "applyView() must not exist on a read-only entry"); } -TEST_F(SLEBaseTests, ApplyViewContextCtor) +TEST_F(SLEBaseTests, apply_view_context_ctor) { ApplyViewImpl av(&env_.getClosedLedger(), TapNone); beast::Journal const j{beast::Journal::getNullSink()}; @@ -260,7 +260,7 @@ TEST_F(SLEBaseTests, ApplyViewContextCtor) EXPECT_EQ(fromCtx.rawSle(), fromView.rawSle()); } -TEST_F(SLEBaseTests, WritableLifecycle) +TEST_F(SLEBaseTests, writable_lifecycle) { // A view we never apply, so nothing here reaches the ledger. ApplyViewImpl av(&env_.getClosedLedger(), TapNone); @@ -318,7 +318,7 @@ TEST_F(SLEBaseTests, WritableLifecycle) } } -TEST_F(SLEBaseTests, Conversion) +TEST_F(SLEBaseTests, conversion) { ApplyViewImpl av(&env_.getClosedLedger(), TapNone); @@ -336,7 +336,7 @@ TEST_F(SLEBaseTests, Conversion) EXPECT_EQ(generic.type(), ltACCOUNT_ROOT); } -TEST_F(SLEBaseTests, ResolveEntryPeeks) +TEST_F(SLEBaseTests, resolve_entry_peeks) { // getOpenLedger() is an OpenView, which derives from ReadView but not // from ApplyView, so resolveEntry's dynamic_cast fails and this takes @@ -368,7 +368,7 @@ TEST_F(SLEBaseTests, ResolveEntryPeeks) EXPECT_EQ(readOnly->getFieldU32(sfSequence), bumped); } -TEST_F(SLEBaseTests, ThrowsOnMissingEntry) +TEST_F(SLEBaseTests, throws_on_missing_entry) { // A generic read-only entry has no static type to fall back on, so // type() must read it off the (absent) SLE and throw. @@ -389,7 +389,7 @@ TEST_F(SLEBaseTests, ThrowsOnMissingEntry) EXPECT_THROW(std::ignore = (*missing).getType(), std::logic_error); } -TEST_F(SLEBaseTests, ThrowsOnMissingWritableEntry) +TEST_F(SLEBaseTests, throws_on_missing_writable_entry) { // A view we never apply, so nothing here reaches the ledger. ApplyViewImpl av(&env_.getClosedLedger(), TapNone); diff --git a/src/tests/libxrpl/ledger/SignerListEntry.cpp b/src/tests/libxrpl/ledger/SignerListEntry.cpp index 8f0e8f5d2d..db65af2f02 100644 --- a/src/tests/libxrpl/ledger/SignerListEntry.cpp +++ b/src/tests/libxrpl/ledger/SignerListEntry.cpp @@ -7,7 +7,7 @@ namespace xrpl::test { -TEST(SignerListEntryTests, Constructors) +TEST(SignerListEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/SponsorshipEntry.cpp b/src/tests/libxrpl/ledger/SponsorshipEntry.cpp index c5004b8d32..735c3e202d 100644 --- a/src/tests/libxrpl/ledger/SponsorshipEntry.cpp +++ b/src/tests/libxrpl/ledger/SponsorshipEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(SponsorshipEntryTests, Constructors) +TEST(SponsorshipEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/TicketEntry.cpp b/src/tests/libxrpl/ledger/TicketEntry.cpp index e09af46cbf..fa26b1a937 100644 --- a/src/tests/libxrpl/ledger/TicketEntry.cpp +++ b/src/tests/libxrpl/ledger/TicketEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(TicketEntryTests, Constructors) +TEST(TicketEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/VaultEntry.cpp b/src/tests/libxrpl/ledger/VaultEntry.cpp index 0cff87bbe9..61f24db410 100644 --- a/src/tests/libxrpl/ledger/VaultEntry.cpp +++ b/src/tests/libxrpl/ledger/VaultEntry.cpp @@ -8,7 +8,7 @@ namespace xrpl::test { -TEST(VaultEntryTests, Constructors) +TEST(VaultEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/XChainOwnedClaimIDEntry.cpp b/src/tests/libxrpl/ledger/XChainOwnedClaimIDEntry.cpp index 1833c68f79..4145471a27 100644 --- a/src/tests/libxrpl/ledger/XChainOwnedClaimIDEntry.cpp +++ b/src/tests/libxrpl/ledger/XChainOwnedClaimIDEntry.cpp @@ -12,7 +12,7 @@ namespace xrpl::test { -TEST(XChainOwnedClaimIDEntryTests, Constructors) +TEST(XChainOwnedClaimIDEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/ledger/XChainOwnedCreateAccountClaimIDEntry.cpp b/src/tests/libxrpl/ledger/XChainOwnedCreateAccountClaimIDEntry.cpp index d109000f77..93851deedf 100644 --- a/src/tests/libxrpl/ledger/XChainOwnedCreateAccountClaimIDEntry.cpp +++ b/src/tests/libxrpl/ledger/XChainOwnedCreateAccountClaimIDEntry.cpp @@ -13,7 +13,7 @@ namespace xrpl::test { -TEST(XChainOwnedCreateAccountClaimIDEntryTests, Constructors) +TEST(XChainOwnedCreateAccountClaimIDEntryTests, constructors) { EntryTestEnv e; diff --git a/src/tests/libxrpl/protocol/STXChainBridge.cpp b/src/tests/libxrpl/protocol/STXChainBridge.cpp index f4e6e60cc9..22f26d7ea0 100644 --- a/src/tests/libxrpl/protocol/STXChainBridge.cpp +++ b/src/tests/libxrpl/protocol/STXChainBridge.cpp @@ -28,7 +28,7 @@ account(std::string_view hex) // getText() builds its string from eight substitutions of the same type, so a // transposed pair would still compile and still type check. Pin the output so // the field/value pairing is actually verified. -TEST(STXChainBridge, getTextPairsEachFieldWithItsValue) +TEST(STXChainBridge, get_text_pairs_each_field_with_its_value) { auto const lockingDoor = account("0102030405060708090A0B0C0D0E0F1011121314"); auto const issuingDoor = account("14131211100F0E0D0C0B0A090807060504030201"); @@ -46,7 +46,7 @@ TEST(STXChainBridge, getTextPairsEachFieldWithItsValue) EXPECT_EQ(bridge.getText(), expected); } -TEST(STXChainBridge, getTextOnADefaultBridge) +TEST(STXChainBridge, get_text_on_a_default_bridge) { STXChainBridge const bridge; auto const text = bridge.getText(); diff --git a/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMirrorUpdateTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMirrorUpdateTests.cpp new file mode 100644 index 0000000000..6cbeb008bd --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/ConfidentialMPTMirrorUpdateTests.cpp @@ -0,0 +1,255 @@ +// Auto-generated unit tests for transaction ConfidentialMPTMirrorUpdate + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsConfidentialMPTMirrorUpdateTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMirrorUpdate")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const holderValue = canonical_ACCOUNT(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const auditorEncryptedAmountValue = canonical_VL(); + auto const zKProofValue = canonical_VL(); + + ConfidentialMPTMirrorUpdateBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + zKProofValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setHolder(holderValue); + builder.setIssuerEncryptedAmount(issuerEncryptedAmountValue); + builder.setAuditorEncryptedAmount(auditorEncryptedAmountValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = tx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = zKProofValue; + auto const actual = tx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + // Verify optional fields + { + auto const& expected = holderValue; + auto const actualOpt = tx.getHolder(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolder should be present"; + expectEqualField(expected, *actualOpt, "sfHolder"); + EXPECT_TRUE(tx.hasHolder()); + } + + { + auto const& expected = issuerEncryptedAmountValue; + auto const actualOpt = tx.getIssuerEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfIssuerEncryptedAmount"); + EXPECT_TRUE(tx.hasIssuerEncryptedAmount()); + } + + { + auto const& expected = auditorEncryptedAmountValue; + auto const actualOpt = tx.getAuditorEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount"); + EXPECT_TRUE(tx.hasAuditorEncryptedAmount()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsConfidentialMPTMirrorUpdateTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMirrorUpdateFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const holderValue = canonical_ACCOUNT(); + auto const issuerEncryptedAmountValue = canonical_VL(); + auto const auditorEncryptedAmountValue = canonical_VL(); + auto const zKProofValue = canonical_VL(); + + // Build an initial transaction + ConfidentialMPTMirrorUpdateBuilder initialBuilder{ + accountValue, + mPTokenIssuanceIDValue, + zKProofValue, + sequenceValue, + feeValue + }; + + initialBuilder.setHolder(holderValue); + initialBuilder.setIssuerEncryptedAmount(issuerEncryptedAmountValue); + initialBuilder.setAuditorEncryptedAmount(auditorEncryptedAmountValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + ConfidentialMPTMirrorUpdateBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actual = rebuiltTx.getMPTokenIssuanceID(); + expectEqualField(expected, actual, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = zKProofValue; + auto const actual = rebuiltTx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + // Verify optional fields + { + auto const& expected = holderValue; + auto const actualOpt = rebuiltTx.getHolder(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfHolder should be present"; + expectEqualField(expected, *actualOpt, "sfHolder"); + } + + { + auto const& expected = issuerEncryptedAmountValue; + auto const actualOpt = rebuiltTx.getIssuerEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfIssuerEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfIssuerEncryptedAmount"); + } + + { + auto const& expected = auditorEncryptedAmountValue; + auto const actualOpt = rebuiltTx.getAuditorEncryptedAmount(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedAmount should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedAmount"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTMirrorUpdateTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTMirrorUpdate{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsConfidentialMPTMirrorUpdateTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(ConfidentialMPTMirrorUpdateBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsConfidentialMPTMirrorUpdateTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testConfidentialMPTMirrorUpdateNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const zKProofValue = canonical_VL(); + + ConfidentialMPTMirrorUpdateBuilder builder{ + accountValue, + mPTokenIssuanceIDValue, + zKProofValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasHolder()); + EXPECT_FALSE(tx.getHolder().has_value()); + EXPECT_FALSE(tx.hasIssuerEncryptedAmount()); + EXPECT_FALSE(tx.getIssuerEncryptedAmount().has_value()); + EXPECT_FALSE(tx.hasAuditorEncryptedAmount()); + EXPECT_FALSE(tx.getAuditorEncryptedAmount().has_value()); +} + +} diff --git a/src/tests/libxrpl/server/InfoSub.cpp b/src/tests/libxrpl/server/InfoSub.cpp index 6913812a92..08698ae3ac 100644 --- a/src/tests/libxrpl/server/InfoSub.cpp +++ b/src/tests/libxrpl/server/InfoSub.cpp @@ -12,7 +12,7 @@ using namespace xrpl; // by subscribing the real cap through a WebSocket, which would exceed the frame // limit and drop the connection before the check runs) lets the boundary be // asserted exactly. -TEST(InfoSubSubscriptionCap, Boundary) +TEST(InfoSubSubscriptionCap, boundary) { constexpr std::size_t cap = kMaxSubscriptionsPerConnection; @@ -30,7 +30,7 @@ TEST(InfoSubSubscriptionCap, Boundary) EXPECT_TRUE(exceedsSubscriptionCap(cap - 1, 2)); } -TEST(InfoSubSubscriptionCap, NoOverflow) +TEST(InfoSubSubscriptionCap, no_overflow) { constexpr std::size_t cap = kMaxSubscriptionsPerConnection; constexpr std::size_t max = std::numeric_limits::max(); @@ -41,7 +41,7 @@ TEST(InfoSubSubscriptionCap, NoOverflow) EXPECT_TRUE(exceedsSubscriptionCap(cap, max)); } -TEST(InfoSubSubscriptionCap, ExplicitCap) +TEST(InfoSubSubscriptionCap, explicit_cap) { // A configured override is honored: the boundary tracks the passed cap, not // the built-in default. This is the seam doSubscribe uses to enforce a diff --git a/src/tests/libxrpl/tx/AccountSet.cpp b/src/tests/libxrpl/tx/AccountSet.cpp index ae291791d4..fe6818670e 100644 --- a/src/tests/libxrpl/tx/AccountSet.cpp +++ b/src/tests/libxrpl/tx/AccountSet.cpp @@ -43,7 +43,7 @@ namespace xrpl::test { -TEST(AccountSet, NullAccountSet) +TEST(AccountSet, null_account_set) { TxTest env; @@ -60,7 +60,7 @@ TEST(AccountSet, NullAccountSet) EXPECT_EQ(accountRoot.getFlags(), 0); } -TEST(AccountSet, MostFlags) +TEST(AccountSet, most_flags) { Account const alice("alice"); @@ -175,7 +175,7 @@ TEST(AccountSet, MostFlags) }); } -TEST(AccountSet, SetAndResetAccountTxnID) +TEST(AccountSet, set_and_reset_account_txn_id) { TxTest env; Account const alice("alice"); @@ -206,7 +206,7 @@ TEST(AccountSet, SetAndResetAccountTxnID) EXPECT_EQ(nowFlags, origFlags); } -TEST(AccountSet, SetNoFreeze) +TEST(AccountSet, set_no_freeze) { TxTest env; Account const alice("alice"); @@ -249,7 +249,7 @@ TEST(AccountSet, SetNoFreeze) EXPECT_TRUE(env.getAccountRoot(alice).isFlag(lsfNoFreeze)); } -TEST(AccountSet, Domain) +TEST(AccountSet, domain) { TxTest env; Account const alice("alice"); @@ -317,7 +317,7 @@ TEST(AccountSet, Domain) } } -TEST(AccountSet, MessageKey) +TEST(AccountSet, message_key) { TxTest env; Account const alice("alice"); @@ -358,7 +358,7 @@ TEST(AccountSet, MessageKey) telBAD_PUBLIC_KEY); } -TEST(AccountSet, WalletID) +TEST(AccountSet, wallet_id) { TxTest env; Account const alice("alice"); @@ -391,7 +391,7 @@ TEST(AccountSet, WalletID) EXPECT_FALSE(env.getAccountRoot(alice).hasWalletLocator()); } -TEST(AccountSet, EmailHash) +TEST(AccountSet, email_hash) { TxTest env; Account const alice("alice"); @@ -422,7 +422,7 @@ TEST(AccountSet, EmailHash) EXPECT_FALSE(env.getAccountRoot(alice).hasEmailHash()); } -TEST(AccountSet, TransferRate) +TEST(AccountSet, transfer_rate) { struct TestCase { @@ -473,7 +473,7 @@ TEST(AccountSet, TransferRate) } } -TEST(AccountSet, BadInputs) +TEST(AccountSet, bad_inputs) { TxTest env; Account const alice("alice"); @@ -553,7 +553,7 @@ TEST(AccountSet, BadInputs) tecNO_ALTERNATIVE_KEY); } -TEST(AccountSet, RequireAuthWithDir) +TEST(AccountSet, require_auth_with_dir) { TxTest env; Account const alice("alice"); @@ -601,7 +601,7 @@ TEST(AccountSet, RequireAuthWithDir) tesSUCCESS); } -TEST(AccountSet, Ticket) +TEST(AccountSet, ticket) { TxTest env; Account const alice("alice"); @@ -660,7 +660,7 @@ TEST(AccountSet, Ticket) tefNO_TICKET); } -TEST(AccountSet, BadSigningKey) +TEST(AccountSet, bad_signing_key) { TxTest env; Account const alice("alice"); @@ -684,7 +684,7 @@ TEST(AccountSet, BadSigningKey) EXPECT_FALSE(result.applied); } -TEST(AccountSet, Gateway) +TEST(AccountSet, gateway) { Account const alice("alice"); Account const bob("bob"); diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp b/src/xrpld/app/ledger/detail/LedgerToJson.cpp index 1d789aa604..419da2a995 100644 --- a/src/xrpld/app/ledger/detail/LedgerToJson.cpp +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +29,7 @@ #include #include +#include #include #include #include @@ -179,6 +182,19 @@ fillJsonTx( } } + // compute outgoing CTID + if (stMeta && stMeta->isFieldPresent(sfTransactionIndex)) + { + uint32_t const lgrSeq = fill.ledger.seq(); + uint32_t const txnIdx = stMeta->getFieldU32(sfTransactionIndex); + uint32_t netID = fill.context->app.getNetworkIDService().getNetworkID(); + if (txn->isFieldPresent(sfNetworkID)) + netID = txn->getFieldU32(sfNetworkID); + + if (auto ctid = rpc::encodeCTID(lgrSeq, txnIdx, netID)) + txJson[jss::ctid] = *ctid; + } + if (((fill.options & static_cast(LedgerFill::Options::OwnerFunds)) != 0) && txn->getTxnType() == ttOFFER_CREATE) { diff --git a/src/xrpld/overlay/detail/ProtocolVersion.cpp b/src/xrpld/overlay/detail/ProtocolVersion.cpp index 74dad61828..d24928a4ec 100644 --- a/src/xrpld/overlay/detail/ProtocolVersion.cpp +++ b/src/xrpld/overlay/detail/ProtocolVersion.cpp @@ -151,4 +151,12 @@ isProtocolSupported(ProtocolVersion const& v) return std::end(kSupportedProtocolList) != std::ranges::find(kSupportedProtocolList, v); } +ProtocolVersion +newestSupportedProtocolVersion() +{ + // Scans rather than reading the sorted list's last entry, so it does not + // depend on an invariant kept elsewhere. + return *std::ranges::max_element(kSupportedProtocolList); +} + } // namespace xrpl diff --git a/src/xrpld/overlay/detail/ProtocolVersion.h b/src/xrpld/overlay/detail/ProtocolVersion.h index 5c05f63e2a..a4342c2453 100644 --- a/src/xrpld/overlay/detail/ProtocolVersion.h +++ b/src/xrpld/overlay/detail/ProtocolVersion.h @@ -68,4 +68,13 @@ supportedProtocolVersions(); bool isProtocolSupported(ProtocolVersion const& v); +/** + * The version negotiated with a peer that speaks everything we speak, so also + * the one that enables every version-gated feature. + * + * @return The largest version in the list of supported protocol versions. + */ +ProtocolVersion +newestSupportedProtocolVersion(); + } // namespace xrpl