diff --git a/.github/scripts/levelization/results/ordering.txt b/.github/scripts/levelization/results/ordering.txt index aee6f4c579..b31e6ae961 100644 --- a/.github/scripts/levelization/results/ordering.txt +++ b/.github/scripts/levelization/results/ordering.txt @@ -159,6 +159,7 @@ test.peerfinder > xrpl.protocol test.protocol > test.jtx test.protocol > test.unit_test test.protocol > xrpl.basics +test.protocol > xrpld.core test.protocol > xrpl.json test.protocol > xrpl.protocol test.rpc > test.jtx diff --git a/include/xrpl/basics/base_uint.h b/include/xrpl/basics/base_uint.h index 96cfa343e3..bee8b8b945 100644 --- a/include/xrpl/basics/base_uint.h +++ b/include/xrpl/basics/base_uint.h @@ -308,7 +308,9 @@ public: XRPL_ASSERT( c.size() * sizeof(typename Container::value_type) == size(), "xrpl::BaseUInt::fromRaw(Container auto) : input size match"); - std::memcpy(result.data_.data(), c.data(), size()); + std::size_t const canCopy = + std::min(size(), c.size() * sizeof(typename Container::value_type)); + std::memcpy(result.data_.data(), c.data(), canCopy); return result; } @@ -322,7 +324,11 @@ public: XRPL_ASSERT( c.size() * sizeof(typename Container::value_type) == size(), "xrpl::BaseUInt::operator=(Container auto) : input size match"); - std::memcpy(data_.data(), c.data(), size()); + std::size_t const canCopy = + std::min(size(), c.size() * sizeof(typename Container::value_type)); + if (canCopy < size()) + *this = beast::kZero; + std::memcpy(data_.data(), c.data(), canCopy); return *this; } diff --git a/src/test/protocol/Issue_test.cpp b/src/test/protocol/Issue_test.cpp index 44a36b7dcf..a6a1fdd341 100644 --- a/src/test/protocol/Issue_test.cpp +++ b/src/test/protocol/Issue_test.cpp @@ -1,10 +1,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include @@ -863,6 +865,101 @@ public: //-------------------------------------------------------------------------- + void + testIssueFromJson() + { + testcase("issueFromJson"); + + // Valid XRP — no issuer field + { + json::Value jv; + jv[jss::currency] = "XRP"; + auto const issue = issueFromJson(jv); + BEAST_EXPECT(isXRP(issue)); + } + + // Valid IOU — legitimate issuer + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + auto const issue = issueFromJson(jv); + BEAST_EXPECT(!isXRP(issue)); + BEAST_EXPECT(issue.account != noAccount()); + } + + // noAccount() is the MPT sentinel in binary serialization - must be + // rejected + try + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = to_string(noAccount()); + issueFromJson(jv); + fail("noAccount() accepted as IOU issuer"); + } + catch (...) + { + pass(); + } + + // xrpAccount() is the XRP sentinel (all zeros) - must be rejected + // as IOU issuer + try + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = to_string(xrpAccount()); + issueFromJson(jv); + fail("xrpAccount() accepted as IOU issuer"); + } + catch (...) + { + pass(); + } + + // Invalid base58 — must be rejected + try + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = "not_a_valid_address"; + issueFromJson(jv); + fail("invalid base58 accepted as IOU issuer"); + } + catch (...) + { + pass(); + } + + // Non-XRP currency with no issuer field — must be rejected + try + { + json::Value jv; + jv[jss::currency] = "USD"; + issueFromJson(jv); + fail("missing issuer accepted"); + } + catch (...) + { + pass(); + } + + // XRP with an issuer field — must be rejected + try + { + json::Value jv; + jv[jss::currency] = "XRP"; + jv[jss::issuer] = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh"; + issueFromJson(jv); + fail("XRP with issuer accepted"); + } + catch (...) + { + pass(); + } + } + void run() override { @@ -897,6 +994,9 @@ public: // --- testIssueDomainSets(); testIssueDomainMaps(); + + // --- + testIssueFromJson(); } }; diff --git a/src/test/protocol/STIssue_test.cpp b/src/test/protocol/STIssue_test.cpp index 1d6d750355..b7cc944e6b 100644 --- a/src/test/protocol/STIssue_test.cpp +++ b/src/test/protocol/STIssue_test.cpp @@ -1,15 +1,24 @@ #include +#include #include // IWYU pragma: keep +#include + +#include #include #include +#include +#include #include #include #include #include #include #include +#include + +#include namespace xrpl::test { @@ -137,12 +146,143 @@ public: "000000000000000000000000000000000000000000000002"); } + void + testNoAccountIssuerRpc() + { + testcase("noAccount issuer rejected via RPC sign"); + + using namespace jtx; + Env env{*this, envconfig([](std::unique_ptr cfg) { + cfg->loadFromString("[signing_support]\ntrue"); + return cfg; + })}; + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + json::Value txJson; + txJson[jss::TransactionType] = "AMMDelete"; + txJson[jss::Account] = alice.human(); + txJson[jss::Asset][jss::currency] = "USD"; + txJson[jss::Asset][jss::issuer] = to_string(noAccount()); + txJson[jss::Asset2][jss::currency] = "XRP"; + + json::Value req; + req[jss::tx_json] = txJson; + req[jss::secret] = alice.name(); + + auto const result = env.rpc("json", "sign", to_string(req))[jss::result]; + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Field 'tx_json.Asset' has invalid data."); + } + + void + testNoAccountIssuer() + { + testcase("noAccount issuer rejection"); + + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = to_string(noAccount()); + + try + { + issueFromJson(sfAsset, jv); + fail("issueFromJson accepted noAccount() as IOU issuer"); + } + catch (...) + { + pass(); + } + } + + { + Serializer s; + s.addBitString(toCurrency("USD")); + s.addBitString(noAccount()); + SerialIter iter(s.slice()); + + try + { + STIssue const stissue(iter, sfAsset); + fail( + "STIssue deserialization of [USD][noAccount()] should " + "throw"); + } + catch (...) + { + pass(); + } + } + } + + void + testXrpAccountIssuerRpc() + { + testcase("xrpAccount issuer rejected via RPC sign"); + + using namespace jtx; + Env env{*this, envconfig([](std::unique_ptr cfg) { + cfg->loadFromString("[signing_support]\ntrue"); + return cfg; + })}; + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + json::Value txJson; + txJson[jss::TransactionType] = "AMMDelete"; + txJson[jss::Account] = alice.human(); + txJson[jss::Asset][jss::currency] = "USD"; + txJson[jss::Asset][jss::issuer] = to_string(xrpAccount()); + txJson[jss::Asset2][jss::currency] = "XRP"; + + json::Value req; + req[jss::tx_json] = txJson; + req[jss::secret] = alice.name(); + + auto const result = env.rpc("json", "sign", to_string(req))[jss::result]; + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Field 'tx_json.Asset' has invalid data."); + } + + void + testXrpAccountIssuer() + { + testcase("xrpAccount issuer rejection"); + + { + json::Value jv; + jv[jss::currency] = "USD"; + jv[jss::issuer] = to_string(xrpAccount()); + + try + { + issueFromJson(sfAsset, jv); + fail("issueFromJson accepted xrpAccount() as IOU issuer"); + } + catch (...) + { + pass(); + } + } + } + void run() override { // compliments other unit tests to ensure complete coverage testConstructor(); testCompare(); + testNoAccountIssuerRpc(); + testNoAccountIssuer(); + testXrpAccountIssuerRpc(); + testXrpAccountIssuer(); } }; diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index 6ba6e7de9f..c656c97a4c 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include #include #include +#include #include namespace xrpl::test { @@ -1616,6 +1618,86 @@ public: } } + void + testAccountObjectDoesntShowCancelledOffers() + { + testcase("AccountObjectDoesntShowCancelledOffers"); + + using namespace jtx; + Env env(*this); + + Account const alice{"alice"}; + Account const bob{"bob"}; + auto const eur = bob["EUR"]; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const rpcAccountObjects = [&](std::optional limit = std::nullopt) { + json::Value params; + params[jss::account] = alice.human(); + if (limit.has_value()) + { + params[jss::limit] = *limit; + } + return env.rpc("json", "account_objects", to_string(params)); + }; + + auto const numEntries = 33; + std::vector seqs; + seqs.reserve(numEntries); + for ([[maybe_unused]] auto _ : std::ranges::iota_view{0, numEntries}) + { + json::Value params; + params[jss::secret] = toBase58(generateSeed("alice")); + params[jss::tx_json] = offer(alice, eur(1), XRP(2)); + auto const res = env.rpc("json", "submit", to_string(params))[jss::result]; + BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS"); + seqs.push_back(env.seq(alice)); + } + + auto res = rpcAccountObjects(); + BEAST_EXPECT(res[jss::result][jss::account_objects].size() == numEntries); + BEAST_EXPECT(not res[jss::result].isMember(jss::limit)); + BEAST_EXPECT(not res[jss::result].isMember(jss::marker)); + + for (auto const s : std::views::all(seqs) | std::views::take(numEntries - 1)) + { + json::Value params; + params[jss::secret] = toBase58(generateSeed("alice")); + params[jss::tx_json] = offerCancel(alice, s - 1); + auto const res = env.rpc("json", "submit", to_string(params))[jss::result]; + BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS"); + } + + res = rpcAccountObjects(); + BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1); + BEAST_EXPECT(not res[jss::result].isMember(jss::limit)); + BEAST_EXPECT(not res[jss::result].isMember(jss::marker)); + + { + json::Value params; + params[jss::secret] = toBase58(generateSeed("alice")); + json::Value txJson; + txJson[jss::TransactionType] = jss::NFTokenMint; + txJson[jss::Account] = to_string(alice.id()); + txJson["NFTokenTaxon"] = 1; + params[jss::tx_json] = txJson; + auto const res = env.rpc("json", "submit", to_string(params))[jss::result]; + BEAST_EXPECT(res[jss::engine_result].asString() == "tesSUCCESS"); + } + env.close(); + + res = rpcAccountObjects(); + BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 2); + BEAST_EXPECT(not res[jss::result].isMember(jss::limit)); + BEAST_EXPECT(not res[jss::result].isMember(jss::marker)); + + res = rpcAccountObjects(1); + BEAST_EXPECT(res[jss::result][jss::account_objects].size() == 1); + BEAST_EXPECT(res[jss::result][jss::limit].asUInt() == 1); + BEAST_EXPECT(res[jss::result].isMember(jss::marker)); + } + void run() override { @@ -1627,6 +1709,7 @@ public: testAccountNFTs(); testAccountObjectMarker(); testSponsoredFilter(); + testAccountObjectDoesntShowCancelledOffers(); } }; diff --git a/src/test/rpc/Transaction_test.cpp b/src/test/rpc/Transaction_test.cpp index c3bf707ec4..4dae475b63 100644 --- a/src/test/rpc/Transaction_test.cpp +++ b/src/test/rpc/Transaction_test.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -880,6 +881,125 @@ class Transaction_test : public beast::unit_test::Suite } } + void + testSignForNetworkIDValidation() + { + testcase("SignFor NetworkID validation"); + using namespace test::jtx; + + Account const owner{"owner"}; + Account const signer{"signer"}; + + auto makeConfig = [](std::uint32_t networkID) { + return envconfig([networkID](std::unique_ptr cfg) { + cfg->networkId = networkID; + return cfg; + }); + }; + + auto setupEnv = [&](Env& env) { + env.fund(XRP(10'000), owner, signer); + env.close(); + env(signers(owner, 1, {{signer, 1}})); + env.close(); + }; + + auto makeTx = [&](Env& env) { + json::Value tx; + tx[jss::TransactionType] = jss::AccountSet; + tx[jss::Account] = owner.human(); + tx[jss::Sequence] = env.seq(owner); + tx[jss::Fee] = "100"; + tx[jss::SigningPubKey] = ""; + return tx; + }; + + auto signFor = [&](Env& env, json::Value const& tx) { + json::Value signReq; + signReq[jss::tx_json] = tx; + signReq[jss::account] = signer.human(); + signReq[jss::secret] = signer.name(); + return env.rpc("json", "sign_for", to_string(signReq))[jss::result]; + }; + + // Test case: NetworkID < 1024 - field is not required + { + Env env{*this, makeConfig(500)}; + setupEnv(env); + + auto tx = makeTx(env); + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::status] == "success"); + BEAST_EXPECT(!result[jss::tx_json].isMember(jss::NetworkID)); + } + + // Test case: NetworkID > 1024 - missing NetworkID field + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Missing field 'tx_json.NetworkID'."); + } + + // Test case: NetworkID > 1024 - NetworkID field is not a number + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + tx[jss::NetworkID] = "not_a_number"; + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'."); + } + + // Test case: NetworkID > 1024 - NetworkID field is not integral + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + tx[jss::NetworkID] = 2040.1; + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'."); + } + + // Test case: NetworkID > 1024 - NetworkID field is different from + // actual NetworkID + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + tx[jss::NetworkID] = 9999; + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::error] == "invalidParams"); + BEAST_EXPECT(result[jss::error_message] == "Invalid field 'tx_json.NetworkID'."); + } + + // Test case: NetworkID > 1024 - NetworkID field is correct + { + Env env{*this, makeConfig(2040)}; + setupEnv(env); + + auto tx = makeTx(env); + tx[jss::NetworkID] = 2040; + auto result = signFor(env, tx); + + BEAST_EXPECT(result[jss::status] == "success"); + BEAST_EXPECT(result[jss::tx_json][jss::NetworkID].asUInt() == 2040); + } + } + public: void run() override @@ -889,6 +1009,8 @@ public: FeatureBitset const all{testableAmendments()}; testWithFeats(all); + + testSignForNetworkIDValidation(); } void diff --git a/src/tests/libxrpl/basics/base_uint_test.cpp b/src/tests/libxrpl/basics/base_uint_test.cpp index eefbff158d..c9bfc35c94 100644 --- a/src/tests/libxrpl/basics/base_uint_test.cpp +++ b/src/tests/libxrpl/basics/base_uint_test.cpp @@ -122,6 +122,73 @@ struct BaseUintTest : public ::testing::Test } }; +using BaseUintDeathTest = BaseUintTest; + +TEST_F(BaseUintDeathTest, fromRaw_size_mismatch) +{ + // ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts. Rather than twist + // these tests into knots to make them work, just skip them. +#ifdef ENABLE_VOIDSTAR + GTEST_SKIP() << "ENABLE_VOIDSTAR is a debug build, but does not crash on failed asserts."; +#else + auto smallConstruct = [] { + // Container smaller than the base_uint (8 bytes vs 12 bytes for + // test96). Only the first 8 bytes are copied; the remaining 4 bytes + // stay zero. + Blob const tooSmall{1, 2, 3, 4, 5, 6, 7, 8}; + BaseUInt96 const result = BaseUInt96::fromRaw(tooSmall); + auto const resultText = to_string(result); + EXPECT_EQ(resultText, "010203040506070800000000") << resultText; + }; + EXPECT_DEBUG_DEATH(smallConstruct(), "input size match"); + + auto largeConstruct = [] { + // Container larger than the base_uint (16 bytes vs 12 bytes for + // test96). Only the first 12 bytes are copied; the extra bytes are + // ignored. + Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + BaseUInt96 const result = BaseUInt96::fromRaw(tooBig); + auto const resultText = to_string(result); + EXPECT_EQ(resultText, "0102030405060708090A0B0C") << resultText; + }; + EXPECT_DEBUG_DEATH(largeConstruct(), "input size match"); + + auto smallCopy = [] { + // Container smaller than the base_uint (8 bytes vs 12 bytes for + // test96). Only the first 8 bytes are copied; the remaining 4 bytes + // stay zero. + Blob const tooSmall{1, 2, 3, 4, 5, 6, 7, 8}; + BaseUInt96 result{}; + --result; + { + auto const originalText = to_string(result); + EXPECT_EQ(originalText, "FFFFFFFFFFFFFFFFFFFFFFFF") << originalText; + } + result = tooSmall; + auto const resultText = to_string(result); + EXPECT_EQ(resultText, "010203040506070800000000") << resultText; + }; + EXPECT_DEBUG_DEATH(smallCopy(), "input size match"); + + auto const largeCopy = [] { + // Container larger than the base_uint (16 bytes vs 12 bytes for + // test96). Only the first 12 bytes are copied; the extra bytes are + // ignored. + Blob const tooBig{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + BaseUInt96 result{}; + --result; + { + auto const originalText = to_string(result); + EXPECT_EQ(originalText, "FFFFFFFFFFFFFFFFFFFFFFFF") << originalText; + } + result = tooBig; + auto const resultText = to_string(result); + EXPECT_EQ(resultText, "0102030405060708090A0B0C") << resultText; + }; + EXPECT_DEBUG_DEATH(largeCopy(), "input size match"); +#endif +} + TEST_F(BaseUintTest, base_uint) { static_assert(!std::is_constructible_v>); @@ -198,6 +265,19 @@ TEST_F(BaseUintTest, base_uint) EXPECT_EQ(d, 0); } + { + // There are several ways to create a zero. beast::kZero is tested above. Test some + // others. + BaseUInt96 const z1; + EXPECT_EQ(z1, z) << to_string(z1); + + BaseUInt96 const z2{}; + EXPECT_EQ(z2, z) << to_string(z2); + + BaseUInt96 const z3{0u}; + EXPECT_EQ(z3, z) << to_string(z3); + } + BaseUInt96 n{z}; n++; EXPECT_EQ(n, BaseUInt96(1));