From 532a471a356a0ea329adca39ec9f30fe5f740233 Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Wed, 27 Nov 2024 10:16:22 +1100 Subject: [PATCH] fixReduceImport (#398) Co-authored-by: Denis Angell --- src/ripple/app/tx/impl/Import.cpp | 39 +++++ src/ripple/protocol/Feature.h | 3 +- src/ripple/protocol/TER.h | 1 + src/ripple/protocol/impl/Feature.cpp | 1 + src/ripple/protocol/impl/TER.cpp | 1 + src/test/app/Import_test.cpp | 216 ++++++++++++++++++++++++++- 6 files changed, 252 insertions(+), 9 deletions(-) diff --git a/src/ripple/app/tx/impl/Import.cpp b/src/ripple/app/tx/impl/Import.cpp index d40a80eba6..335cbe5817 100644 --- a/src/ripple/app/tx/impl/Import.cpp +++ b/src/ripple/app/tx/impl/Import.cpp @@ -889,6 +889,45 @@ Import::preclaim(PreclaimContext const& ctx) } auto const& sle = ctx.view.read(keylet::account(ctx.tx[sfAccount])); + + auto const tt = stpTrans->getTxnType(); + if ((tt == ttSIGNER_LIST_SET || tt == ttREGULAR_KEY_SET) && + ctx.view.rules().enabled(fixReduceImport) && sle) + { + // blackhole check + do + { + // if master key is not set then it is not blackholed + if (!(sle->getFlags() & lsfDisableMaster)) + break; + + // if a regular key is set then it must be acc 0, 1, or 2 otherwise + // not blackholed + if (sle->isFieldPresent(sfRegularKey)) + { + AccountID rk = sle->getAccountID(sfRegularKey); + static const AccountID ACCOUNT_ZERO(0); + static const AccountID ACCOUNT_ONE(1); + static const AccountID ACCOUNT_TWO(2); + + if (rk != ACCOUNT_ZERO && rk != ACCOUNT_ONE && + rk != ACCOUNT_TWO) + break; + } + + // if a signer list is set then it's not blackholed + auto const signerListKeylet = keylet::signers(ctx.tx[sfAccount]); + if (ctx.view.exists(signerListKeylet)) + break; + + // execution to here means it's blackholed + JLOG(ctx.j.warn()) + << "Import: during preclaim target account is blackholed " + << ctx.tx[sfAccount] << ", bailing."; + return tefIMPORT_BLACKHOLED; + } while (0); + } + if (sle && sle->isFieldPresent(sfImportSequence)) { uint32_t sleImportSequence = sle->getFieldU32(sfImportSequence); diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 43d510c636..715f5dac6e 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -74,7 +74,7 @@ namespace detail { // Feature.cpp. Because it's only used to reserve storage, and determine how // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // the actual number of amendments. A LogicError on startup will verify this. -static constexpr std::size_t numFeatures = 74; +static constexpr std::size_t numFeatures = 75; /** Amendments that this server supports and the default voting behavior. Whether they are enabled depends on the Rules defined in the validated @@ -362,6 +362,7 @@ extern uint256 const fix240819; extern uint256 const fixPageCap; extern uint256 const fix240911; extern uint256 const fixFloatDivide; +extern uint256 const fixReduceImport; } // namespace ripple diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index 42d3cabd35..7cd3cae422 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -184,6 +184,7 @@ enum TEFcodes : TERUnderlyingType { tefPAST_IMPORT_SEQ, tefPAST_IMPORT_VL_SEQ, tefNONDIR_EMIT, + tefIMPORT_BLACKHOLED, }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index 23cbe236df..1c7fc931b1 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -468,6 +468,7 @@ REGISTER_FIX (fix240819, Supported::yes, VoteBehavior::De REGISTER_FIX (fixPageCap, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fix240911, Supported::yes, VoteBehavior::DefaultYes); REGISTER_FIX (fixFloatDivide, Supported::yes, VoteBehavior::DefaultYes); +REGISTER_FIX (fixReduceImport, Supported::yes, VoteBehavior::DefaultYes); // The following amendments are obsolete, but must remain supported // because they could potentially get enabled. diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index be8be1dd42..e41134a0c4 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -116,6 +116,7 @@ transResults() MAKE_ERROR(tefNO_TICKET, "Ticket is not in ledger."), MAKE_ERROR(tefNFTOKEN_IS_NOT_TRANSFERABLE, "The specified NFToken is not transferable."), MAKE_ERROR(tefNONDIR_EMIT, "An emitted txn was injected into the ledger without a corresponding directory entry."), + MAKE_ERROR(tefIMPORT_BLACKHOLED, "Cannot import keying because target account is blackholed."), MAKE_ERROR(telLOCAL_ERROR, "Local failure."), MAKE_ERROR(telBAD_DOMAIN, "Domain too long."), diff --git a/src/test/app/Import_test.cpp b/src/test/app/Import_test.cpp index c115bcc4d4..09ffd4a023 100644 --- a/src/test/app/Import_test.cpp +++ b/src/test/app/Import_test.cpp @@ -79,7 +79,7 @@ class Import_test : public beast::unit_test::suite importVLSequence(jtx::Env const& env, PublicKey const& pk) { auto const sle = env.le(keylet::import_vlseq(pk)); - if (sle->isFieldPresent(sfImportSequence)) + if (sle && sle->isFieldPresent(sfImportSequence)) return (*sle)[sfImportSequence]; return 0; } @@ -2672,6 +2672,134 @@ class Import_test : public beast::unit_test::suite env(import::import(alice, tmpXpop), ter(temMALFORMED)); } + // tefIMPORT_BLACKHOLED - SetRegularKey (w/seed) AccountZero + { + test::jtx::Env env{ + *this, network::makeNetworkVLConfig(21337, keys)}; + auto const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + env.fund(XRP(1000), alice); + env.close(); + + // Set Regular Key + Json::Value jv; + jv[jss::Account] = alice.human(); + const AccountID ACCOUNT_ZERO(0); + jv["RegularKey"] = to_string(ACCOUNT_ZERO); + jv[jss::TransactionType] = jss::SetRegularKey; + env(jv, alice); + + // Disable Master Key + env(fset(alice, asfDisableMaster), sig(alice)); + env.close(); + + // Import with Master Key + Json::Value tmpXpop = + import::loadXpop(ImportTCSetRegularKey::w_seed); + env(import::import(alice, tmpXpop), + ter(tefIMPORT_BLACKHOLED), + fee(feeDrops * 10), + sig(alice)); + env.close(); + } + + // tefIMPORT_BLACKHOLED - SetRegularKey (w/seed) AccountOne + { + test::jtx::Env env{ + *this, network::makeNetworkVLConfig(21337, keys)}; + auto const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + env.fund(XRP(1000), alice); + env.close(); + + // Set Regular Key + Json::Value jv; + jv[jss::Account] = alice.human(); + const AccountID ACCOUNT_ONE(1); + jv["RegularKey"] = to_string(ACCOUNT_ONE); + jv[jss::TransactionType] = jss::SetRegularKey; + env(jv, alice); + + // Disable Master Key + env(fset(alice, asfDisableMaster), sig(alice)); + env.close(); + + // Import with Master Key + Json::Value tmpXpop = + import::loadXpop(ImportTCSetRegularKey::w_seed); + env(import::import(alice, tmpXpop), + ter(tefIMPORT_BLACKHOLED), + fee(feeDrops * 10), + sig(alice)); + env.close(); + } + + // tefIMPORT_BLACKHOLED - SetRegularKey (w/seed) AccountTwo + { + test::jtx::Env env{ + *this, network::makeNetworkVLConfig(21337, keys)}; + auto const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + env.fund(XRP(1000), alice); + env.close(); + + // Set Regular Key + Json::Value jv; + jv[jss::Account] = alice.human(); + const AccountID ACCOUNT_TWO(2); + jv["RegularKey"] = to_string(ACCOUNT_TWO); + jv[jss::TransactionType] = jss::SetRegularKey; + env(jv, alice); + + // Disable Master Key + env(fset(alice, asfDisableMaster), sig(alice)); + env.close(); + + // Import with Master Key + Json::Value tmpXpop = + import::loadXpop(ImportTCSetRegularKey::w_seed); + env(import::import(alice, tmpXpop), + ter(tefIMPORT_BLACKHOLED), + fee(feeDrops * 10), + sig(alice)); + env.close(); + } + + // tefIMPORT_BLACKHOLED - SignersListSet (w/seed) + { + test::jtx::Env env{ + *this, network::makeNetworkVLConfig(21337, keys)}; + auto const feeDrops = env.current()->fees().base; + + auto const alice = Account("alice"); + env.fund(XRP(1000), alice); + env.close(); + + // Set Regular Key + Json::Value jv; + jv[jss::Account] = alice.human(); + const AccountID ACCOUNT_ZERO(0); + jv["RegularKey"] = to_string(ACCOUNT_ZERO); + jv[jss::TransactionType] = jss::SetRegularKey; + env(jv, alice); + + // Disable Master Key + env(fset(alice, asfDisableMaster), sig(alice)); + env.close(); + + // Import with Master Key + Json::Value tmpXpop = + import::loadXpop(ImportTCSignersListSet::w_seed); + env(import::import(alice, tmpXpop), + ter(tefIMPORT_BLACKHOLED), + fee(feeDrops * 10), + sig(alice)); + env.close(); + } + // tefPAST_IMPORT_SEQ { test::jtx::Env env{ @@ -4580,14 +4708,22 @@ class Import_test : public beast::unit_test::suite // confirm signers set auto const [signers, signersSle] = signersKeyAndSle(*env.current(), alice); - auto const signerEntries = - signersSle->getFieldArray(sfSignerEntries); - BEAST_EXPECT(signerEntries.size() == 2); - BEAST_EXPECT(signerEntries[0u].getFieldU16(sfSignerWeight) == 1); BEAST_EXPECT( - signerEntries[0u].getAccountID(sfAccount) == carol.id()); - BEAST_EXPECT(signerEntries[1u].getFieldU16(sfSignerWeight) == 1); - BEAST_EXPECT(signerEntries[1u].getAccountID(sfAccount) == bob.id()); + signersSle && signersSle->isFieldPresent(sfSignerEntries)); + if (signersSle && signersSle->isFieldPresent(sfSignerEntries)) + { + auto const signerEntries = + signersSle->getFieldArray(sfSignerEntries); + BEAST_EXPECT(signerEntries.size() == 2); + BEAST_EXPECT( + signerEntries[0u].getFieldU16(sfSignerWeight) == 1); + BEAST_EXPECT( + signerEntries[0u].getAccountID(sfAccount) == carol.id()); + BEAST_EXPECT( + signerEntries[1u].getFieldU16(sfSignerWeight) == 1); + BEAST_EXPECT( + signerEntries[1u].getAccountID(sfAccount) == bob.id()); + } // confirm multisign tx env.close(); @@ -5986,6 +6122,69 @@ class Import_test : public beast::unit_test::suite } } + void + testBlackhole(FeatureBitset features) + { + testcase("blackhole"); + + using namespace test::jtx; + using namespace std::literals; + + auto blackholeAccount = [&](Env& env, Account const& acct) { + // Set Regular Key + Json::Value jv; + jv[jss::Account] = acct.human(); + const AccountID ACCOUNT_ZERO(0); + jv["RegularKey"] = to_string(ACCOUNT_ZERO); + jv[jss::TransactionType] = jss::SetRegularKey; + env(jv, acct); + + // Disable Master Key + env(fset(acct, asfDisableMaster), sig(acct)); + env.close(); + }; + + auto burnHeader = [&](Env& env) { + // confirm total coins header + auto const initCoins = env.current()->info().drops; + BEAST_EXPECT(initCoins == 100'000'000'000'000'000); + + // burn 10'000 xrp + auto const master = Account("masterpassphrase"); + env(noop(master), fee(100'000'000'000'000), ter(tesSUCCESS)); + env.close(); + + // confirm total coins header + auto const burnCoins = env.current()->info().drops; + BEAST_EXPECT(burnCoins == initCoins - 100'000'000'000'000); + }; + + // AccountSet (w/seed) + { + test::jtx::Env env{ + *this, network::makeNetworkVLConfig(21337, keys)}; + auto const feeDrops = env.current()->fees().base; + + // Burn Header + burnHeader(env); + + auto const alice = Account("alice"); + env.fund(XRP(1000), alice); + env.close(); + + // Blackhole Account + blackholeAccount(env, alice); + + // Import with Master Key + Json::Value tmpXpop = import::loadXpop(ImportTCAccountSet::w_seed); + env(import::import(alice, tmpXpop), + ter(tesSUCCESS), + fee(feeDrops * 10), + sig(alice)); + env.close(); + } + } + public: void run() override @@ -6026,6 +6225,7 @@ class Import_test : public beast::unit_test::suite testMaxSupply(features); testMinMax(features); testHalving(features - featureOwnerPaysFee); + testBlackhole(features); } };