mirror of
https://github.com/Xahau/xahaud.git
synced 2025-12-06 17:27:52 +00:00
Merge branch 'dev' into sync-2.4.0
This commit is contained in:
@@ -80,7 +80,7 @@ namespace detail {
|
|||||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
// 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
|
// 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.
|
// the actual number of amendments. A LogicError on startup will verify this.
|
||||||
static constexpr std::size_t numFeatures = 103;
|
static constexpr std::size_t numFeatures = 104;
|
||||||
|
|
||||||
/** Amendments that this server supports and the default voting behavior.
|
/** Amendments that this server supports and the default voting behavior.
|
||||||
Whether they are enabled depends on the Rules defined in the validated
|
Whether they are enabled depends on the Rules defined in the validated
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ XRPL_FEATURE(XChainBridge, Supported::no, VoteBehavior::DefaultNo
|
|||||||
XRPL_FEATURE(AMM, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FEATURE(AMM, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (ReducedOffersV1, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FIX (ReducedOffersV1, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
|
XRPL_FIX (ProvisionalDoubleThreading, Supported::yes, VoteBehavior::DefaultYes)
|
||||||
XRPL_FIX (RewardClaimFlags, Supported::yes, VoteBehavior::DefaultYes)
|
XRPL_FIX (RewardClaimFlags, Supported::yes, VoteBehavior::DefaultYes)
|
||||||
XRPL_FEATURE(HookCanEmit, Supported::yes, VoteBehavior::DefaultNo)
|
XRPL_FEATURE(HookCanEmit, Supported::yes, VoteBehavior::DefaultNo)
|
||||||
XRPL_FIX (20250131, Supported::yes, VoteBehavior::DefaultYes)
|
XRPL_FIX (20250131, Supported::yes, VoteBehavior::DefaultYes)
|
||||||
|
|||||||
@@ -3285,7 +3285,7 @@ private:
|
|||||||
affected, 4u + 1u))) // 4u + 1u(Issuer Account as Touch)
|
affected, 4u + 1u))) // 4u + 1u(Issuer Account as Touch)
|
||||||
return;
|
return;
|
||||||
auto ff =
|
auto ff =
|
||||||
affected[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
|
affected[3u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
|
||||||
BEAST_EXPECT(
|
BEAST_EXPECT(
|
||||||
ff[sfHighLimit.fieldName] ==
|
ff[sfHighLimit.fieldName] ==
|
||||||
bob["USD"](100).value().getJson(JsonOptions::none));
|
bob["USD"](100).value().getJson(JsonOptions::none));
|
||||||
@@ -3561,10 +3561,10 @@ private:
|
|||||||
auto ff =
|
auto ff =
|
||||||
affected[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
|
affected[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
|
||||||
BEAST_EXPECT(
|
BEAST_EXPECT(
|
||||||
ff[sfLowLimit.fieldName] ==
|
ff[sfHighLimit.fieldName] ==
|
||||||
G1["USD"](0).value().getJson(JsonOptions::none));
|
G1["USD"](0).value().getJson(JsonOptions::none));
|
||||||
BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfLowFreeze);
|
BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfLowFreeze));
|
||||||
BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfHighFreeze));
|
BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfHighFreeze);
|
||||||
env.close();
|
env.close();
|
||||||
|
|
||||||
// test: Can make a payment via the new offer
|
// test: Can make a payment via the new offer
|
||||||
|
|||||||
317
src/test/app/PreviousTxn_test.cpp
Normal file
317
src/test/app/PreviousTxn_test.cpp
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2025 Ripple Labs Inc.
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <test/jtx.h>
|
||||||
|
#include <xrpl/protocol/jss.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace test {
|
||||||
|
|
||||||
|
class PreviousTxnID_test : public beast::unit_test::suite
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void
|
||||||
|
testPreviousTxnID(FeatureBitset features)
|
||||||
|
{
|
||||||
|
using namespace test::jtx;
|
||||||
|
Env env{
|
||||||
|
*this, envconfig(), features, nullptr, beast::severities::kNone};
|
||||||
|
auto j = env.app().logs().journal("PreviousTxnID_test");
|
||||||
|
|
||||||
|
auto const alice = Account{"alice"};
|
||||||
|
auto const bob = Account{"bob"};
|
||||||
|
auto const USD = bob["USD"];
|
||||||
|
|
||||||
|
env.fund(XRP(10000), alice, bob);
|
||||||
|
env.close();
|
||||||
|
|
||||||
|
// Create a trustline
|
||||||
|
env(trust(alice, USD(1000)));
|
||||||
|
env.close();
|
||||||
|
|
||||||
|
// Get the transaction metadata and ID
|
||||||
|
auto const meta1 = env.meta();
|
||||||
|
auto const trustCreateTxID = env.tx()->getTransactionID();
|
||||||
|
BEAST_EXPECT(meta1);
|
||||||
|
|
||||||
|
// Check if ModifiedNode has PreviousTxnID at root level
|
||||||
|
auto const& affectedNodes = meta1->getFieldArray(sfAffectedNodes);
|
||||||
|
bool foundPreviousTxnID = false;
|
||||||
|
bool foundModifiedRippleState = false;
|
||||||
|
bool foundCreatedRippleState = false;
|
||||||
|
|
||||||
|
for (auto const& node : affectedNodes)
|
||||||
|
{
|
||||||
|
if (node.getFieldU16(sfLedgerEntryType) == ltRIPPLE_STATE)
|
||||||
|
{
|
||||||
|
if (node.getFName() == sfModifiedNode)
|
||||||
|
{
|
||||||
|
BEAST_EXPECT(false);
|
||||||
|
}
|
||||||
|
else if (node.getFName() == sfCreatedNode)
|
||||||
|
{
|
||||||
|
foundCreatedRippleState = true;
|
||||||
|
foundPreviousTxnID = node.isFieldPresent(sfPreviousTxnID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BEAST_EXPECT(foundCreatedRippleState);
|
||||||
|
// Why we expect NO PreviousTxnID in newly created trustline metadata:
|
||||||
|
//
|
||||||
|
// PreviousTxnID in metadata indicates which transaction PREVIOUSLY
|
||||||
|
// modified an object, not which transaction created it. For a newly
|
||||||
|
// created object, there is no previous transaction - the current
|
||||||
|
// transaction is the first one to touch this object.
|
||||||
|
//
|
||||||
|
// While the SLE itself will have PreviousTxnID set to the creating
|
||||||
|
// transaction (by ApplyStateTable::threadItem), the metadata correctly
|
||||||
|
// omits PreviousTxnID from CreatedNode because there was no previous
|
||||||
|
// modification to reference.
|
||||||
|
//
|
||||||
|
// Technical detail: trustCreate() creates the RippleState with
|
||||||
|
// PreviousTxnID as a defaultObject placeholder (since it's soeREQUIRED
|
||||||
|
// in LedgerFormats.cpp). ApplyStateTable::generateTxMeta skips fields
|
||||||
|
// with zero/empty values when generating metadata, so the unset
|
||||||
|
// PreviousTxnID doesn't appear in the CreatedNode output.
|
||||||
|
BEAST_EXPECT(foundPreviousTxnID == false);
|
||||||
|
BEAST_EXPECT(foundModifiedRippleState == false);
|
||||||
|
|
||||||
|
// Now let's check the actual ledger entry to see if PreviousTxnID was
|
||||||
|
// set Get the trustline from the ledger
|
||||||
|
auto const trustlineKey = keylet::line(alice, bob, USD.currency);
|
||||||
|
auto const sleTrustline = env.le(trustlineKey);
|
||||||
|
BEAST_EXPECT(sleTrustline);
|
||||||
|
|
||||||
|
if (sleTrustline)
|
||||||
|
{
|
||||||
|
// Check if the SLE itself has PreviousTxnID set
|
||||||
|
// Even though it didn't appear in metadata, the SLE should have it
|
||||||
|
// because ApplyStateTable::threadItem() sets it after creation
|
||||||
|
bool sleHasPrevTxnID =
|
||||||
|
sleTrustline->isFieldPresent(sfPreviousTxnID);
|
||||||
|
bool sleHasPrevTxnSeq =
|
||||||
|
sleTrustline->isFieldPresent(sfPreviousTxnLgrSeq);
|
||||||
|
|
||||||
|
JLOG(j.info()) << "SLE has PreviousTxnID: " << sleHasPrevTxnID;
|
||||||
|
JLOG(j.info()) << "SLE has PreviousTxnLgrSeq: " << sleHasPrevTxnSeq;
|
||||||
|
|
||||||
|
if (sleHasPrevTxnID && sleHasPrevTxnSeq)
|
||||||
|
{
|
||||||
|
auto const prevTxnID =
|
||||||
|
sleTrustline->getFieldH256(sfPreviousTxnID);
|
||||||
|
auto const prevTxnSeq =
|
||||||
|
sleTrustline->getFieldU32(sfPreviousTxnLgrSeq);
|
||||||
|
auto const creatingTxnID = env.tx()->getTransactionID();
|
||||||
|
|
||||||
|
JLOG(j.info()) << "SLE PreviousTxnID: " << prevTxnID;
|
||||||
|
JLOG(j.info()) << "Creating TxnID: " << creatingTxnID;
|
||||||
|
JLOG(j.info()) << "SLE PreviousTxnLgrSeq: " << prevTxnSeq;
|
||||||
|
|
||||||
|
// The PreviousTxnID in the SLE should match the transaction
|
||||||
|
// that created it
|
||||||
|
BEAST_EXPECT(prevTxnID == creatingTxnID);
|
||||||
|
BEAST_EXPECT(prevTxnSeq == env.closed()->seq());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This would indicate that ApplyStateTable::threadItem() didn't
|
||||||
|
// set PreviousTxnID on the newly created trustline
|
||||||
|
JLOG(j.warn())
|
||||||
|
<< "Newly created trustline SLE missing PreviousTxnID!";
|
||||||
|
BEAST_EXPECT(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now modify the trustline with a payment
|
||||||
|
env(pay(bob, alice, USD(100)));
|
||||||
|
env.close();
|
||||||
|
|
||||||
|
// Get the second transaction metadata
|
||||||
|
auto const meta2 = env.meta();
|
||||||
|
BEAST_EXPECT(meta2);
|
||||||
|
|
||||||
|
// Check ModifiedNode for PreviousTxnID
|
||||||
|
auto const& affectedNodes2 = meta2->getFieldArray(sfAffectedNodes);
|
||||||
|
bool foundPreviousTxnIDInModified = false;
|
||||||
|
bool foundPreviousTxnIDInPreviousFields = false;
|
||||||
|
|
||||||
|
for (auto const& node : affectedNodes2)
|
||||||
|
{
|
||||||
|
if (node.getFName() == sfModifiedNode &&
|
||||||
|
node.getFieldU16(sfLedgerEntryType) == ltRIPPLE_STATE)
|
||||||
|
{
|
||||||
|
auto json = node.getJson(JsonOptions::none);
|
||||||
|
JLOG(j.trace()) << json;
|
||||||
|
|
||||||
|
// Why we expect PreviousTxnID in ModifiedNode metadata:
|
||||||
|
//
|
||||||
|
// When a transaction modifies an existing RippleState, the
|
||||||
|
// ApplyView tracks the modification. During metadata
|
||||||
|
// generation, ApplyStateTable::generateTxMeta compares the
|
||||||
|
// before and after states of the SLE.
|
||||||
|
//
|
||||||
|
// For ModifiedNode entries, the metadata should include
|
||||||
|
// PreviousTxnID and PreviousTxnLgrSeq at the root level to
|
||||||
|
// indicate which transaction last modified this object.
|
||||||
|
// This is different from PreviousFields, which shows what
|
||||||
|
// field values changed.
|
||||||
|
//
|
||||||
|
// The bug fixed by the `fixProvisionalDoubleThreading`
|
||||||
|
// amendment was that ApplyStateTable::threadItem() was
|
||||||
|
// modifying the original SLE during provisional metadata
|
||||||
|
// generation. This contaminated the "before" state used for
|
||||||
|
// comparison, so when final metadata was generated, the
|
||||||
|
// comparison didn't see PreviousTxnID as a change because both
|
||||||
|
// states had the new value.
|
||||||
|
bool expectPreviousTxnID =
|
||||||
|
features[fixProvisionalDoubleThreading];
|
||||||
|
|
||||||
|
if (node.isFieldPresent(sfPreviousTxnID))
|
||||||
|
{
|
||||||
|
foundPreviousTxnIDInModified = true;
|
||||||
|
auto prevTxnID = node.getFieldH256(sfPreviousTxnID);
|
||||||
|
auto prevLgrSeq = node.getFieldU32(sfPreviousTxnLgrSeq);
|
||||||
|
|
||||||
|
BEAST_EXPECT(node.isFieldPresent(sfPreviousTxnLgrSeq));
|
||||||
|
BEAST_EXPECT(prevTxnID != beast::zero);
|
||||||
|
BEAST_EXPECT(prevLgrSeq > 0);
|
||||||
|
|
||||||
|
JLOG(j.info()) << "Found PreviousTxnID: " << prevTxnID
|
||||||
|
<< " at ledger: " << prevLgrSeq << std::endl;
|
||||||
|
|
||||||
|
// When the fix is enabled, we should see the trustline
|
||||||
|
// creation transaction ID as the previous transaction
|
||||||
|
if (expectPreviousTxnID)
|
||||||
|
{
|
||||||
|
BEAST_EXPECT(prevTxnID == trustCreateTxID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Without the fix, we expect PreviousTxnID to be missing
|
||||||
|
// due to the provisional metadata contamination bug
|
||||||
|
JLOG(j.info()) << "PreviousTxnID missing in metadata";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if PreviousTxnID appears in PreviousFields
|
||||||
|
// (it shouldn't - PreviousTxnID is a root-level field)
|
||||||
|
if (node.isFieldPresent(sfPreviousFields))
|
||||||
|
{
|
||||||
|
auto prevFields = dynamic_cast<STObject const*>(
|
||||||
|
node.peekAtPField(sfPreviousFields));
|
||||||
|
if (prevFields &&
|
||||||
|
prevFields->isFieldPresent(sfPreviousTxnID))
|
||||||
|
{
|
||||||
|
foundPreviousTxnIDInPreviousFields = true;
|
||||||
|
JLOG(j.warn()) << "Found PreviousTxnID in "
|
||||||
|
"PreviousFields (unexpected)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreviousTxnID should never appear in PreviousFields
|
||||||
|
BEAST_EXPECT(!foundPreviousTxnIDInPreviousFields);
|
||||||
|
|
||||||
|
// With the fix enabled, we expect to find PreviousTxnID
|
||||||
|
// Without the fix, we expect it to be missing (the bug)
|
||||||
|
if (features[fixProvisionalDoubleThreading])
|
||||||
|
{
|
||||||
|
BEAST_EXPECT(foundPreviousTxnIDInModified);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BEAST_EXPECT(!foundPreviousTxnIDInModified);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional check: Verify the SLE state after the payment
|
||||||
|
auto const sleTrustlineAfter = env.le(trustlineKey);
|
||||||
|
BEAST_EXPECT(sleTrustlineAfter);
|
||||||
|
|
||||||
|
if (sleTrustlineAfter)
|
||||||
|
{
|
||||||
|
// The SLE should always have PreviousTxnID set after modification
|
||||||
|
BEAST_EXPECT(sleTrustlineAfter->isFieldPresent(sfPreviousTxnID));
|
||||||
|
BEAST_EXPECT(
|
||||||
|
sleTrustlineAfter->isFieldPresent(sfPreviousTxnLgrSeq));
|
||||||
|
|
||||||
|
auto const currentPrevTxnID =
|
||||||
|
sleTrustlineAfter->getFieldH256(sfPreviousTxnID);
|
||||||
|
auto const currentPrevTxnSeq =
|
||||||
|
sleTrustlineAfter->getFieldU32(sfPreviousTxnLgrSeq);
|
||||||
|
|
||||||
|
// The PreviousTxnID should now point to the payment transaction
|
||||||
|
auto const paymentTxID = env.tx()->getTransactionID();
|
||||||
|
BEAST_EXPECT(currentPrevTxnID == paymentTxID);
|
||||||
|
BEAST_EXPECT(currentPrevTxnSeq == env.closed()->seq());
|
||||||
|
|
||||||
|
// When the bug is present (feature disabled), the metadata won't
|
||||||
|
// show the change, but the SLE will still be updated correctly
|
||||||
|
if (!features[fixProvisionalDoubleThreading])
|
||||||
|
{
|
||||||
|
JLOG(j.info())
|
||||||
|
<< "Bug confirmed: SLE has correct PreviousTxnID ("
|
||||||
|
<< currentPrevTxnID
|
||||||
|
<< ") but metadata doesn't show the change";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check account objects were threaded correctly
|
||||||
|
auto const aliceAccount = env.le(keylet::account(alice));
|
||||||
|
auto const bobAccount = env.le(keylet::account(bob));
|
||||||
|
|
||||||
|
BEAST_EXPECT(aliceAccount);
|
||||||
|
BEAST_EXPECT(bobAccount);
|
||||||
|
|
||||||
|
if (aliceAccount && bobAccount)
|
||||||
|
{
|
||||||
|
// Both accounts should have been threaded by the payment
|
||||||
|
BEAST_EXPECT(aliceAccount->isFieldPresent(sfPreviousTxnID));
|
||||||
|
BEAST_EXPECT(bobAccount->isFieldPresent(sfPreviousTxnID));
|
||||||
|
|
||||||
|
auto const alicePrevTxnID =
|
||||||
|
aliceAccount->getFieldH256(sfPreviousTxnID);
|
||||||
|
auto const bobPrevTxnID = bobAccount->getFieldH256(sfPreviousTxnID);
|
||||||
|
auto const paymentTxID = env.tx()->getTransactionID();
|
||||||
|
|
||||||
|
// Both should point to the payment transaction
|
||||||
|
BEAST_EXPECT(alicePrevTxnID == paymentTxID);
|
||||||
|
BEAST_EXPECT(bobPrevTxnID == paymentTxID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
run() override
|
||||||
|
{
|
||||||
|
using namespace test::jtx;
|
||||||
|
auto const sa = supported_amendments();
|
||||||
|
|
||||||
|
testcase("With fixProvisionalDoubleThreading enabled");
|
||||||
|
testPreviousTxnID(sa);
|
||||||
|
|
||||||
|
testcase("Without fixProvisionalDoubleThreading (bug present)");
|
||||||
|
testPreviousTxnID(sa - fixProvisionalDoubleThreading);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
BEAST_DEFINE_TESTSUITE(PreviousTxnID, app, ripple);
|
||||||
|
|
||||||
|
} // namespace test
|
||||||
|
} // namespace ripple
|
||||||
@@ -185,7 +185,8 @@ class TxQPosNegFlows_test : public beast::unit_test::suite
|
|||||||
|
|
||||||
// In order for the vote to occur, we must run as a validator
|
// In order for the vote to occur, we must run as a validator
|
||||||
p->section("validation_seed")
|
p->section("validation_seed")
|
||||||
.legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH");
|
.legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH"); // not-suspicious
|
||||||
|
// test seed
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -5057,7 +5058,9 @@ public:
|
|||||||
testFailInPreclaim(all);
|
testFailInPreclaim(all);
|
||||||
testQueuedTxFails(all);
|
testQueuedTxFails(all);
|
||||||
testMultiTxnPerAccount(all);
|
testMultiTxnPerAccount(all);
|
||||||
testTieBreaking(all);
|
// fragile: hardcoded ordering by txID XOR parentHash
|
||||||
|
// parentHash < txTree Hash < txMeta < PreviousTxnID
|
||||||
|
testTieBreaking(all - fixProvisionalDoubleThreading);
|
||||||
testAcctTxnID(all);
|
testAcctTxnID(all);
|
||||||
testMaximum(all);
|
testMaximum(all);
|
||||||
testUnexpectedBalanceChange(all);
|
testUnexpectedBalanceChange(all);
|
||||||
@@ -5075,7 +5078,9 @@ public:
|
|||||||
testAcctInQueueButEmpty(all);
|
testAcctInQueueButEmpty(all);
|
||||||
testRPC(all);
|
testRPC(all);
|
||||||
testExpirationReplacement(all);
|
testExpirationReplacement(all);
|
||||||
testFullQueueGapFill(all);
|
// fragile: hardcoded ordering by txID XOR parentHash
|
||||||
|
// parentHash < txTree Hash < txMeta < PreviousTxnID
|
||||||
|
testFullQueueGapFill(all - fixProvisionalDoubleThreading);
|
||||||
testSignAndSubmitSequence(all);
|
testSignAndSubmitSequence(all);
|
||||||
testAccountInfo(all);
|
testAccountInfo(all);
|
||||||
testServerInfo(all);
|
testServerInfo(all);
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ class AccountTx_test : public beast::unit_test::suite
|
|||||||
(payment[jss::validated] == true) &&
|
(payment[jss::validated] == true) &&
|
||||||
(payment[jss::ledger_index] == 3) &&
|
(payment[jss::ledger_index] == 3) &&
|
||||||
(payment[jss::ledger_hash] ==
|
(payment[jss::ledger_hash] ==
|
||||||
"6A55EEBF003EB79C1BF0B8FF75CBF448161F60E3366808820"
|
"6B1FECE09EE027F4D035A1C0DDE3562E527606AF97B57EF3B"
|
||||||
"06903B7CC077EF2") &&
|
"E259617D67C8F37") &&
|
||||||
(payment[jss::close_time_iso] ==
|
(payment[jss::close_time_iso] ==
|
||||||
"2000-01-01T00:00:10Z");
|
"2000-01-01T00:00:10Z");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3306,7 +3306,8 @@ public:
|
|||||||
section.set("normal_consensus_increase_percent", "0");
|
section.set("normal_consensus_increase_percent", "0");
|
||||||
return cfg;
|
return cfg;
|
||||||
}),
|
}),
|
||||||
supported_amendments() - featureXahauGenesis};
|
supported_amendments() - featureXahauGenesis -
|
||||||
|
fixProvisionalDoubleThreading};
|
||||||
|
|
||||||
Json::Value jv;
|
Json::Value jv;
|
||||||
jv[jss::ledger_index] = "current";
|
jv[jss::ledger_index] = "current";
|
||||||
|
|||||||
@@ -349,8 +349,8 @@ public:
|
|||||||
== "2000-01-01T00:00:10Z" &&
|
== "2000-01-01T00:00:10Z" &&
|
||||||
jv[jss::validated] == true && //
|
jv[jss::validated] == true && //
|
||||||
jv[jss::ledger_hash] ==
|
jv[jss::ledger_hash] ==
|
||||||
"8730420CE799AD878693358C0C927A72407E4D032E951C854399890312"
|
"3BD88B8E93BED46C0B1ACB2C46687DE29F19F9BB82DE6C3D8CC491D6AE"
|
||||||
"116400" && //
|
"DEE517" && //
|
||||||
!jv[jss::inLedger] &&
|
!jv[jss::inLedger] &&
|
||||||
jv[jss::ledger_index] == 3 && //
|
jv[jss::ledger_index] == 3 && //
|
||||||
jv[jss::tx_json][jss::TransactionType] //
|
jv[jss::tx_json][jss::TransactionType] //
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
|||||||
"TransactionType" : "AccountSet",
|
"TransactionType" : "AccountSet",
|
||||||
"TxnSignature" : "3044022007B35E3B99460534FF6BC3A66FBBA03591C355CC38E38588968E87CCD01BE229022071A443026DE45041B55ABB1CC76812A87EA701E475BBB7E165513B4B242D3474",
|
"TxnSignature" : "3044022007B35E3B99460534FF6BC3A66FBBA03591C355CC38E38588968E87CCD01BE229022071A443026DE45041B55ABB1CC76812A87EA701E475BBB7E165513B4B242D3474",
|
||||||
})",
|
})",
|
||||||
"9E73E138359D4EB17B7F51F5F910E27F7920E9C50713E6D805A8AA8A87F9A43B",
|
"5E8D88365131CA2EA3E0ADAFFB4A927D16B3820744D60B1303A6D9398358693D",
|
||||||
"2000-01-01T00:00:10Z");
|
"2000-01-01T00:00:10Z");
|
||||||
check_tx(
|
check_tx(
|
||||||
env.closed()->seq(),
|
env.closed()->seq(),
|
||||||
@@ -293,7 +293,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
|||||||
"TransactionType" : "AccountSet",
|
"TransactionType" : "AccountSet",
|
||||||
"TxnSignature" : "3045022100C8857FC0759A2AC0D2F320684691A66EAD252EAED9EF88C79791BC58BFCC9D860220421722286487DD0ED6BBA626CE6FCBDD14289F7F4726870C3465A4054C2702D7",
|
"TxnSignature" : "3045022100C8857FC0759A2AC0D2F320684691A66EAD252EAED9EF88C79791BC58BFCC9D860220421722286487DD0ED6BBA626CE6FCBDD14289F7F4726870C3465A4054C2702D7",
|
||||||
})",
|
})",
|
||||||
"9E73E138359D4EB17B7F51F5F910E27F7920E9C50713E6D805A8AA8A87F9A43B",
|
"5E8D88365131CA2EA3E0ADAFFB4A927D16B3820744D60B1303A6D9398358693D",
|
||||||
"2000-01-01T00:00:10Z");
|
"2000-01-01T00:00:10Z");
|
||||||
|
|
||||||
env.trust(A2["USD"](1000), A1);
|
env.trust(A2["USD"](1000), A1);
|
||||||
@@ -328,7 +328,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
|||||||
"TransactionType" : "Payment",
|
"TransactionType" : "Payment",
|
||||||
"TxnSignature" : "3044022033D9EBF7F02950AF2F6B13C07AEE641C8FEBDD540A338FCB9027A965A4AED35B02206E4E227DCC226A3456C0FEF953449D21645A24EB63CA0BB7C5B62470147FD1D1",
|
"TxnSignature" : "3044022033D9EBF7F02950AF2F6B13C07AEE641C8FEBDD540A338FCB9027A965A4AED35B02206E4E227DCC226A3456C0FEF953449D21645A24EB63CA0BB7C5B62470147FD1D1",
|
||||||
})",
|
})",
|
||||||
"C0B5ECAE7066D926A7C486F0AC6A234DDD8468A4021C8D851A6CA57CE0467CB5",
|
"41DB005149858C9BE599AE98FE1DF5BD4E9E0265B53DD4792532E9296A0773B6",
|
||||||
"2000-01-01T00:00:20Z");
|
"2000-01-01T00:00:20Z");
|
||||||
|
|
||||||
check_tx(
|
check_tx(
|
||||||
@@ -350,7 +350,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
|||||||
"TransactionType" : "Payment",
|
"TransactionType" : "Payment",
|
||||||
"TxnSignature" : "30450221008A722B7F16EDB2348886E88ED4EC682AE9973CC1EE0FF37C93BB2CEC821D3EDF022059E464472031BA5E0D88A93E944B6A8B8DB3E1D5E5D1399A805F615789DB0BED",
|
"TxnSignature" : "30450221008A722B7F16EDB2348886E88ED4EC682AE9973CC1EE0FF37C93BB2CEC821D3EDF022059E464472031BA5E0D88A93E944B6A8B8DB3E1D5E5D1399A805F615789DB0BED",
|
||||||
})",
|
})",
|
||||||
"C0B5ECAE7066D926A7C486F0AC6A234DDD8468A4021C8D851A6CA57CE0467CB5",
|
"41DB005149858C9BE599AE98FE1DF5BD4E9E0265B53DD4792532E9296A0773B6",
|
||||||
"2000-01-01T00:00:20Z");
|
"2000-01-01T00:00:20Z");
|
||||||
|
|
||||||
env(offer(A2, XRP(100), A2["USD"](1)));
|
env(offer(A2, XRP(100), A2["USD"](1)));
|
||||||
@@ -379,7 +379,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
|||||||
"TransactionType" : "OfferCreate",
|
"TransactionType" : "OfferCreate",
|
||||||
"TxnSignature" : "304502210093FC93ACB77B4E3DE3315441BD010096734859080C1797AB735EB47EBD541BD102205020BB1A7C3B4141279EE4C287C13671E2450EA78914EFD0C6DB2A18344CD4F2",
|
"TxnSignature" : "304502210093FC93ACB77B4E3DE3315441BD010096734859080C1797AB735EB47EBD541BD102205020BB1A7C3B4141279EE4C287C13671E2450EA78914EFD0C6DB2A18344CD4F2",
|
||||||
})",
|
})",
|
||||||
"EED1C1D4364BBA9955EA0D603B45273B4F2B8D089A428F8787EAD625551AD342",
|
"87622B7BC61E8CD01E7C5FC5198CA5A255F8468983CE1E19245FEE3068D97968",
|
||||||
"2000-01-01T00:00:30Z");
|
"2000-01-01T00:00:30Z");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -821,8 +821,8 @@ class Transaction_test : public beast::unit_test::suite
|
|||||||
BEAST_EXPECT(result[jss::result][jss::ledger_index] == 4);
|
BEAST_EXPECT(result[jss::result][jss::ledger_index] == 4);
|
||||||
BEAST_EXPECT(
|
BEAST_EXPECT(
|
||||||
result[jss::result][jss::ledger_hash] ==
|
result[jss::result][jss::ledger_hash] ==
|
||||||
"F7A5A71945502089F557ECACD71EAB0A0B4E99BED527C2BF2DCD40C52130ED"
|
"34BBC578F3A4EB6FC6C192C38F99EADD512316B32FA60B425764F1F9602DBB"
|
||||||
"BC");
|
"00");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto memberIt = expected.begin(); memberIt != expected.end();
|
for (auto memberIt = expected.begin(); memberIt != expected.end();
|
||||||
@@ -897,8 +897,8 @@ class Transaction_test : public beast::unit_test::suite
|
|||||||
result[jss::result][jss::meta_blob] == expected_meta_blob);
|
result[jss::result][jss::meta_blob] == expected_meta_blob);
|
||||||
BEAST_EXPECT(
|
BEAST_EXPECT(
|
||||||
result[jss::result][jss::ledger_hash] ==
|
result[jss::result][jss::ledger_hash] ==
|
||||||
"892EBD0B2801B921DC20E8AA82914F3C1DD54B1789E9CFB1966F717C01"
|
"6BE57FA882745536BF528B09E0BAD3F31FC7CDA0284DDD0E0B97540550"
|
||||||
"73B2C8");
|
"FFBECF");
|
||||||
BEAST_EXPECT(
|
BEAST_EXPECT(
|
||||||
result[jss::result][jss::close_time_iso] ==
|
result[jss::result][jss::close_time_iso] ==
|
||||||
"2000-01-01T00:00:10Z");
|
"2000-01-01T00:00:10Z");
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ ApplyStateTable::generateTxMeta(
|
|||||||
std::optional<STAmount> const& deliver,
|
std::optional<STAmount> const& deliver,
|
||||||
std::vector<STObject> const& hookExecution,
|
std::vector<STObject> const& hookExecution,
|
||||||
std::vector<STObject> const& hookEmission,
|
std::vector<STObject> const& hookEmission,
|
||||||
beast::Journal j)
|
beast::Journal j,
|
||||||
|
bool isProvisional)
|
||||||
{
|
{
|
||||||
TxMeta meta(tx.getTransactionID(), to.seq());
|
TxMeta meta(tx.getTransactionID(), to.seq());
|
||||||
if (deliver)
|
if (deliver)
|
||||||
@@ -202,7 +203,7 @@ ApplyStateTable::generateTxMeta(
|
|||||||
|
|
||||||
if (curNode->isThreadedType(to.rules())) // thread transaction to
|
if (curNode->isThreadedType(to.rules())) // thread transaction to
|
||||||
// node item modified
|
// node item modified
|
||||||
threadItem(meta, curNode);
|
threadItem(meta, curNode, to.rules());
|
||||||
|
|
||||||
STObject prevs(sfPreviousFields);
|
STObject prevs(sfPreviousFields);
|
||||||
for (auto const& obj : *origNode)
|
for (auto const& obj : *origNode)
|
||||||
@@ -238,7 +239,7 @@ ApplyStateTable::generateTxMeta(
|
|||||||
threadOwners(to, meta, curNode, newMod, j);
|
threadOwners(to, meta, curNode, newMod, j);
|
||||||
|
|
||||||
if (curNode->isThreadedType(to.rules())) // always thread to self
|
if (curNode->isThreadedType(to.rules())) // always thread to self
|
||||||
threadItem(meta, curNode);
|
threadItem(meta, curNode, to.rules());
|
||||||
|
|
||||||
STObject news(sfNewFields);
|
STObject news(sfNewFields);
|
||||||
for (auto const& obj : *curNode)
|
for (auto const& obj : *curNode)
|
||||||
@@ -265,6 +266,34 @@ ApplyStateTable::generateTxMeta(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After provisional metadata generation, restore the original PreviousTxnID
|
||||||
|
// values to prevent contamination of the "before" state used for final
|
||||||
|
// metadata comparison. This ensures PreviousTxnID appears correctly in
|
||||||
|
// ModifiedNode metadata.
|
||||||
|
if (isProvisional && to.rules().enabled(fixProvisionalDoubleThreading))
|
||||||
|
{
|
||||||
|
for (auto const& [key, state] : originalThreadingState_)
|
||||||
|
{
|
||||||
|
auto iter = items_.find(key);
|
||||||
|
if (iter != items_.end())
|
||||||
|
{
|
||||||
|
auto sle = iter->second.second;
|
||||||
|
if (state.hasPrevTxnID)
|
||||||
|
{
|
||||||
|
sle->setFieldH256(sfPreviousTxnID, state.prevTxnID);
|
||||||
|
sle->setFieldU32(sfPreviousTxnLgrSeq, state.prevTxnLgrSeq);
|
||||||
|
// Restored to original PreviousTxnID
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sle->makeFieldAbsent(sfPreviousTxnID);
|
||||||
|
sle->makeFieldAbsent(sfPreviousTxnLgrSeq);
|
||||||
|
// Restored to no PreviousTxnID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {meta, newMod};
|
return {meta, newMod};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,6 +334,7 @@ ApplyStateTable::apply(
|
|||||||
JLOG(j.trace()) << "metadata " << meta.getJson(JsonOptions::none);
|
JLOG(j.trace()) << "metadata " << meta.getJson(JsonOptions::none);
|
||||||
|
|
||||||
metadata = meta;
|
metadata = meta;
|
||||||
|
// Metadata has been generated
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDryRun)
|
if (!isDryRun)
|
||||||
@@ -572,9 +602,51 @@ ApplyStateTable::destroyXRP(XRPAmount const& fee)
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
// Insert this transaction to the SLE's threading list
|
// Insert this transaction to the SLE's threading list
|
||||||
|
//
|
||||||
|
// This method is called during metadata generation to update the
|
||||||
|
// PreviousTxnID/PreviousTxnLgrSeq fields on SLEs. However, it's called
|
||||||
|
// twice for each transaction:
|
||||||
|
// 1. During provisional metadata generation (for hooks to see)
|
||||||
|
// 2. During final metadata generation (for the actual ledger)
|
||||||
|
//
|
||||||
|
// The fixProvisionalDoubleThreading amendment fixes a bug where the
|
||||||
|
// provisional threading would contaminate the "original" state used
|
||||||
|
// for metadata comparison, causing PreviousTxnID to be missing from
|
||||||
|
// the final metadata.
|
||||||
|
//
|
||||||
|
// The fix works by:
|
||||||
|
// - Saving the original PreviousTxnID state for an SLE the first time it's
|
||||||
|
// threaded during the provisional pass.
|
||||||
|
// - Restoring the original state for all affected SLEs in a single batch
|
||||||
|
// after the entire provisional metadata generation is complete.
|
||||||
|
//
|
||||||
|
// This batch-restore is critical because threadItem() can be called on the
|
||||||
|
// same SLE multiple times within one metadata pass. Restoring immediately
|
||||||
|
// would be incorrect. This approach ensures the final metadata comparison
|
||||||
|
// starts from the correct, uncontaminated "before" state.
|
||||||
void
|
void
|
||||||
ApplyStateTable::threadItem(TxMeta& meta, std::shared_ptr<SLE> const& sle)
|
ApplyStateTable::threadItem(
|
||||||
|
TxMeta& meta,
|
||||||
|
std::shared_ptr<SLE> const& sle,
|
||||||
|
const Rules& rules)
|
||||||
{
|
{
|
||||||
|
if (rules.enabled(fixProvisionalDoubleThreading))
|
||||||
|
{
|
||||||
|
auto const key = sle->key();
|
||||||
|
if (originalThreadingState_.find(key) == originalThreadingState_.end())
|
||||||
|
{
|
||||||
|
// First time (provisional metadata) - save the original state
|
||||||
|
ThreadingState state;
|
||||||
|
state.hasPrevTxnID = sle->isFieldPresent(sfPreviousTxnID);
|
||||||
|
if (state.hasPrevTxnID)
|
||||||
|
{
|
||||||
|
state.prevTxnID = sle->getFieldH256(sfPreviousTxnID);
|
||||||
|
state.prevTxnLgrSeq = sle->getFieldU32(sfPreviousTxnLgrSeq);
|
||||||
|
}
|
||||||
|
originalThreadingState_[key] = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
key_type prevTxID;
|
key_type prevTxID;
|
||||||
LedgerIndex prevLgrID;
|
LedgerIndex prevLgrID;
|
||||||
|
|
||||||
@@ -593,6 +665,11 @@ ApplyStateTable::threadItem(TxMeta& meta, std::shared_ptr<SLE> const& sle)
|
|||||||
"set");
|
"set");
|
||||||
node.setFieldH256(sfPreviousTxnID, prevTxID);
|
node.setFieldH256(sfPreviousTxnID, prevTxID);
|
||||||
node.setFieldU32(sfPreviousTxnLgrSeq, prevLgrID);
|
node.setFieldU32(sfPreviousTxnLgrSeq, prevLgrID);
|
||||||
|
// Added PreviousTxnID to metadata
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// PreviousTxnID already present in metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
@@ -677,7 +754,7 @@ ApplyStateTable::threadTx(
|
|||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
sle->isThreadedType(base.rules()),
|
sle->isThreadedType(base.rules()),
|
||||||
"ripple::ApplyStateTable::threadTx : SLE is threaded");
|
"ripple::ApplyStateTable::threadTx : SLE is threaded");
|
||||||
threadItem(meta, sle);
|
threadItem(meta, sle, base.rules());
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <xrpld/ledger/RawView.h>
|
#include <xrpld/ledger/RawView.h>
|
||||||
#include <xrpld/ledger/ReadView.h>
|
#include <xrpld/ledger/ReadView.h>
|
||||||
#include <xrpl/beast/utility/Journal.h>
|
#include <xrpl/beast/utility/Journal.h>
|
||||||
|
#include <xrpl/protocol/Rules.h>
|
||||||
#include <xrpl/protocol/TER.h>
|
#include <xrpl/protocol/TER.h>
|
||||||
#include <xrpl/protocol/TxMeta.h>
|
#include <xrpl/protocol/TxMeta.h>
|
||||||
#include <xrpl/protocol/XRPAmount.h>
|
#include <xrpl/protocol/XRPAmount.h>
|
||||||
@@ -53,6 +54,18 @@ private:
|
|||||||
items_t items_;
|
items_t items_;
|
||||||
XRPAmount dropsDestroyed_{0};
|
XRPAmount dropsDestroyed_{0};
|
||||||
|
|
||||||
|
// Track original PreviousTxnID/LgrSeq values to restore after provisional
|
||||||
|
// metadata. This map is populated during provisional metadata generation
|
||||||
|
// and consumed during final metadata generation. It is not cleared as
|
||||||
|
// the ApplyStateTable instance is single-use per transaction.
|
||||||
|
struct ThreadingState
|
||||||
|
{
|
||||||
|
uint256 prevTxnID;
|
||||||
|
uint32_t prevTxnLgrSeq;
|
||||||
|
bool hasPrevTxnID;
|
||||||
|
};
|
||||||
|
mutable std::map<key_type, ThreadingState> originalThreadingState_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ApplyStateTable() = default;
|
ApplyStateTable() = default;
|
||||||
ApplyStateTable(ApplyStateTable&&) = default;
|
ApplyStateTable(ApplyStateTable&&) = default;
|
||||||
@@ -73,7 +86,8 @@ public:
|
|||||||
std::optional<STAmount> const& deliver,
|
std::optional<STAmount> const& deliver,
|
||||||
std::vector<STObject> const& hookExecution,
|
std::vector<STObject> const& hookExecution,
|
||||||
std::vector<STObject> const& hookEmission,
|
std::vector<STObject> const& hookEmission,
|
||||||
beast::Journal j);
|
beast::Journal j,
|
||||||
|
bool isProvisional = false);
|
||||||
|
|
||||||
std::optional<TxMeta>
|
std::optional<TxMeta>
|
||||||
apply(
|
apply(
|
||||||
@@ -139,8 +153,11 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void
|
void
|
||||||
threadItem(TxMeta& meta, std::shared_ptr<SLE> const& to);
|
threadItem(
|
||||||
|
TxMeta& meta,
|
||||||
|
std::shared_ptr<SLE> const& to,
|
||||||
|
Rules const& rules);
|
||||||
|
|
||||||
std::shared_ptr<SLE>
|
std::shared_ptr<SLE>
|
||||||
getForMod(
|
getForMod(
|
||||||
|
|||||||
@@ -47,8 +47,13 @@ ApplyViewImpl::generateProvisionalMeta(
|
|||||||
beast::Journal j)
|
beast::Journal j)
|
||||||
{
|
{
|
||||||
auto [meta, _] = items_.generateTxMeta(
|
auto [meta, _] = items_.generateTxMeta(
|
||||||
to, tx, deliver_, hookExecution_, hookEmission_, j);
|
to,
|
||||||
|
tx,
|
||||||
|
deliver_,
|
||||||
|
hookExecution_,
|
||||||
|
hookEmission_,
|
||||||
|
j,
|
||||||
|
true); // isProvisional = true
|
||||||
return meta;
|
return meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user