Compare commits

..

1 Commits

7 changed files with 168 additions and 34 deletions

View File

@@ -2,9 +2,6 @@
#include <xrpl/basics/IntrusivePointer.ipp> #include <xrpl/basics/IntrusivePointer.ipp>
#include <xrpl/basics/TaggedCache.h> #include <xrpl/basics/TaggedCache.h>
#include <xrpl/basics/scope.h>
#include <algorithm>
namespace xrpl { namespace xrpl {
@@ -598,37 +595,8 @@ TaggedCache<Key, T, IsKeyCache, SharedWeakUnionPointer, SharedPointerType, Hash,
std::vector<key_type> v; std::vector<key_type> v;
{ {
// Keep track of how many iterations are needed. Exit the loop if the number of retries gets std::scoped_lock const lock(mutex_);
// absurd. (Note that if this somehow ever happens, one more allocation will be done under v.reserve(cache_.size());
// lock, which is undesirable, but really should be almost impossible. Also, assert that
// there were fewer than 3 needed after the loop, because in a normal operating environment,
// even 2 is going to be unusual, and 3 shouldn't be needed.
std::size_t allocationIterations = 0;
std::unique_lock lock(mutex_);
for (auto size = cache_.size(); v.capacity() < size && allocationIterations < 20;
size = cache_.size())
{
ScopeUnlock const unlock(lock);
// Allocate the current size plus a little extra, in case the cache grows while
// allocating. Each time another allocation is needed, the extra also gets bigger until
// it ultimately doubles the size + 1.
size += (size >> (4 - std::min(allocationIterations, std::size_t{4}))) + 1;
v.reserve(size);
++allocationIterations;
}
XRPL_ASSERT(
allocationIterations < 3,
"xrpl::TaggedCache::getKeys(): limited allocation iterations");
if (v.capacity() < cache_.size())
{
// LCOV_EXCL_START
UNREACHABLE("xrpl::TaggedCache::getKeys(): failed to allocate sufficient capacity");
v.reserve(cache_.size());
// LCOV_EXCL_STOP
}
XRPL_ASSERT(lock.owns_lock(), "xrpl::TaggedCache::getKeys(): owns lock");
XRPL_ASSERT(
v.capacity() >= cache_.size(), "xrpl::TaggedCache::getKeys(): sufficient capacity");
for (auto const& _ : cache_) for (auto const& _ : cache_)
v.push_back(_.first); v.push_back(_.first);
} }

View File

@@ -14,6 +14,7 @@
// Add new amendments to the top of this list. // Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order. // Keep it sorted in reverse chronological order.
XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo) XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo)

View File

@@ -889,6 +889,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
MustDeleteAcct | DestroyMptIssuance | MustModifyVault, MustDeleteAcct | DestroyMptIssuance | MustModifyVault,
({ ({
{sfVaultID, SoeRequired}, {sfVaultID, SoeRequired},
{sfMemoData, SoeOptional},
})) }))
/** This transaction trades assets for shares with a vault. */ /** This transaction trades assets for shares with a vault. */

View File

@@ -57,6 +57,32 @@ public:
{ {
return this->tx_->at(sfVaultID); return this->tx_->at(sfVaultID);
} }
/**
* @brief Get sfMemoData (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getMemoData() const
{
if (hasMemoData())
{
return this->tx_->at(sfMemoData);
}
return std::nullopt;
}
/**
* @brief Check if sfMemoData is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasMemoData() const
{
return this->tx_->isFieldPresent(sfMemoData);
}
}; };
/** /**
@@ -112,6 +138,17 @@ public:
return *this; return *this;
} }
/**
* @brief Set sfMemoData (SoeOptional)
* @return Reference to this builder for method chaining.
*/
VaultDeleteBuilder&
setMemoData(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfMemoData] = value;
return *this;
}
/** /**
* @brief Build and return the VaultDelete wrapper. * @brief Build and return the VaultDelete wrapper.
* @param publicKey The public key for signing. * @param publicKey The public key for signing.

View File

@@ -7,8 +7,10 @@
#include <xrpl/ledger/helpers/MPTokenHelpers.h> #include <xrpl/ledger/helpers/MPTokenHelpers.h>
#include <xrpl/ledger/helpers/TokenHelpers.h> #include <xrpl/ledger/helpers/TokenHelpers.h>
#include <xrpl/protocol/AccountID.h> #include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h> #include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/MPTIssue.h> #include <xrpl/protocol/MPTIssue.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/SField.h> #include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STLedgerEntry.h> #include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STNumber.h> // IWYU pragma: keep #include <xrpl/protocol/STNumber.h> // IWYU pragma: keep
@@ -28,6 +30,12 @@ VaultDelete::preflight(PreflightContext const& ctx)
return temMALFORMED; return temMALFORMED;
} }
if (ctx.tx.isFieldPresent(sfMemoData) && !ctx.rules.enabled(featureLendingProtocolV1_1))
return temDISABLED;
if (!validDataLength(ctx.tx[~sfMemoData], kMaxDataPayloadLength))
return temMALFORMED;
return tesSUCCESS; return tesSUCCESS;
} }

View File

@@ -7511,6 +7511,74 @@ class Vault_test : public beast::unit_test::Suite
} }
} }
void
testVaultDeleteMemoData()
{
using namespace test::jtx;
Env env{*this};
Account const owner{"owner"};
env.fund(XRP(1'000'000), owner);
env.close();
Vault const vault{env};
auto const keylet = keylet::vault(owner.id(), 1);
auto delTx = vault.del({.owner = owner, .id = keylet.key});
// Test VaultDelete with featureLendingProtocolV1_1 disabled
// Transaction fails if the data field is provided
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 disabled");
env.disableFeature(featureLendingProtocolV1_1);
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
env(delTx, Ter(temDISABLED));
env.enableFeature(featureLendingProtocolV1_1);
env.close();
}
// Transaction fails if the data field is too large
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data too large");
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength + 1, 'A'));
env(delTx, Ter(temMALFORMED));
env.close();
}
// Transaction fails if the data field is set, but is empty
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data empty");
delTx[sfMemoData] = strHex(std::string(0, 'A'));
env(delTx, Ter(temMALFORMED));
env.close();
}
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled no vault");
auto const keylet = keylet::vault(owner.id(), env.seq(owner));
// Recreate the transaction as the vault keylet changed
auto delTx = vault.del({.owner = owner, .id = keylet.key});
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
env(delTx, Ter(tecNO_ENTRY));
env.close();
}
{
testcase("VaultDelete memo data featureLendingProtocolV1_1 enabled data valid");
PrettyAsset const xrpAsset = xrpIssue();
auto const [tx, keylet] = vault.create({.owner = owner, .asset = xrpAsset});
env(tx, Ter(tesSUCCESS));
env.close();
// Recreate the transaction as the vault keylet changed
auto delTx = vault.del({.owner = owner, .id = keylet.key});
delTx[sfMemoData] = strHex(std::string(kMaxDataPayloadLength, 'A'));
env(delTx, Ter(tesSUCCESS));
env.close();
}
}
void void
testVaultDepositFreeze() testVaultDepositFreeze()
{ {
@@ -8082,6 +8150,7 @@ class Vault_test : public beast::unit_test::Suite
runTests(); runTests();
env.disableFeature(fixCleanup3_3_0); env.disableFeature(fixCleanup3_3_0);
runTests(); runTests();
env.enableFeature(fixCleanup3_3_0); env.enableFeature(fixCleanup3_3_0);
} }
@@ -8115,6 +8184,7 @@ public:
testVaultClawbackAssets(); testVaultClawbackAssets();
testVaultEscrowedMPT(); testVaultEscrowedMPT();
testAssetsMaximum(); testAssetsMaximum();
testVaultDeleteMemoData();
testBug6LimitBypassWithShares(); testBug6LimitBypassWithShares();
testRemoveEmptyHoldingLockedAmount(); testRemoveEmptyHoldingLockedAmount();
testRemoveEmptyHoldingConfidentialBalances(); testRemoveEmptyHoldingConfidentialBalances();

View File

@@ -30,6 +30,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
// Transaction-specific field values // Transaction-specific field values
auto const vaultIDValue = canonical_UINT256(); auto const vaultIDValue = canonical_UINT256();
auto const memoDataValue = canonical_VL();
VaultDeleteBuilder builder{ VaultDeleteBuilder builder{
accountValue, accountValue,
@@ -39,6 +40,7 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
}; };
// Set optional fields // Set optional fields
builder.setMemoData(memoDataValue);
auto tx = builder.build(publicKey, secretKey); auto tx = builder.build(publicKey, secretKey);
@@ -62,6 +64,14 @@ TEST(TransactionsVaultDeleteTests, BuilderSettersRoundTrip)
} }
// Verify optional fields // Verify optional fields
{
auto const& expected = memoDataValue;
auto const actualOpt = tx.getMemoData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
expectEqualField(expected, *actualOpt, "sfMemoData");
EXPECT_TRUE(tx.hasMemoData());
}
} }
// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, // 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper,
@@ -79,6 +89,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
// Transaction-specific field values // Transaction-specific field values
auto const vaultIDValue = canonical_UINT256(); auto const vaultIDValue = canonical_UINT256();
auto const memoDataValue = canonical_VL();
// Build an initial transaction // Build an initial transaction
VaultDeleteBuilder initialBuilder{ VaultDeleteBuilder initialBuilder{
@@ -88,6 +99,7 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
feeValue feeValue
}; };
initialBuilder.setMemoData(memoDataValue);
auto initialTx = initialBuilder.build(publicKey, secretKey); auto initialTx = initialBuilder.build(publicKey, secretKey);
@@ -112,6 +124,13 @@ TEST(TransactionsVaultDeleteTests, BuilderFromStTxRoundTrip)
} }
// Verify optional fields // Verify optional fields
{
auto const& expected = memoDataValue;
auto const actualOpt = rebuiltTx.getMemoData();
ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMemoData should be present";
expectEqualField(expected, *actualOpt, "sfMemoData");
}
} }
// 3) Verify wrapper throws when constructed from wrong transaction type. // 3) Verify wrapper throws when constructed from wrong transaction type.
@@ -142,5 +161,35 @@ TEST(TransactionsVaultDeleteTests, BuilderThrowsOnWrongTxType)
EXPECT_THROW(VaultDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error); EXPECT_THROW(VaultDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error);
} }
// 5) Build with only required fields and verify optional fields return nullopt.
TEST(TransactionsVaultDeleteTests, OptionalFieldsReturnNullopt)
{
// Generate a deterministic keypair for signing
auto const [publicKey, secretKey] =
generateKeyPair(KeyType::Secp256k1, generateSeed("testVaultDeleteNullopt"));
// 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 vaultIDValue = canonical_UINT256();
VaultDeleteBuilder builder{
accountValue,
vaultIDValue,
sequenceValue,
feeValue
};
// Do NOT set optional fields
auto tx = builder.build(publicKey, secretKey);
// Verify optional fields are not present
EXPECT_FALSE(tx.hasMemoData());
EXPECT_FALSE(tx.getMemoData().has_value());
}
} }