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
|
||||
// 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 = 103;
|
||||
static constexpr std::size_t numFeatures = 104;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
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(Clawback, 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_FEATURE(HookCanEmit, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20250131, Supported::yes, VoteBehavior::DefaultYes)
|
||||
|
||||
@@ -3285,7 +3285,7 @@ private:
|
||||
affected, 4u + 1u))) // 4u + 1u(Issuer Account as Touch)
|
||||
return;
|
||||
auto ff =
|
||||
affected[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
|
||||
affected[3u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
|
||||
BEAST_EXPECT(
|
||||
ff[sfHighLimit.fieldName] ==
|
||||
bob["USD"](100).value().getJson(JsonOptions::none));
|
||||
@@ -3561,10 +3561,10 @@ private:
|
||||
auto ff =
|
||||
affected[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName];
|
||||
BEAST_EXPECT(
|
||||
ff[sfLowLimit.fieldName] ==
|
||||
ff[sfHighLimit.fieldName] ==
|
||||
G1["USD"](0).value().getJson(JsonOptions::none));
|
||||
BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfLowFreeze);
|
||||
BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfHighFreeze));
|
||||
BEAST_EXPECT(!(ff[jss::Flags].asUInt() & lsfLowFreeze));
|
||||
BEAST_EXPECT(ff[jss::Flags].asUInt() & lsfHighFreeze);
|
||||
env.close();
|
||||
|
||||
// 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
|
||||
p->section("validation_seed")
|
||||
.legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH");
|
||||
.legacy("shUwVw52ofnCUX5m7kPTKzJdr4HEH"); // not-suspicious
|
||||
// test seed
|
||||
}
|
||||
return p;
|
||||
}
|
||||
@@ -5057,7 +5058,9 @@ public:
|
||||
testFailInPreclaim(all);
|
||||
testQueuedTxFails(all);
|
||||
testMultiTxnPerAccount(all);
|
||||
testTieBreaking(all);
|
||||
// fragile: hardcoded ordering by txID XOR parentHash
|
||||
// parentHash < txTree Hash < txMeta < PreviousTxnID
|
||||
testTieBreaking(all - fixProvisionalDoubleThreading);
|
||||
testAcctTxnID(all);
|
||||
testMaximum(all);
|
||||
testUnexpectedBalanceChange(all);
|
||||
@@ -5075,7 +5078,9 @@ public:
|
||||
testAcctInQueueButEmpty(all);
|
||||
testRPC(all);
|
||||
testExpirationReplacement(all);
|
||||
testFullQueueGapFill(all);
|
||||
// fragile: hardcoded ordering by txID XOR parentHash
|
||||
// parentHash < txTree Hash < txMeta < PreviousTxnID
|
||||
testFullQueueGapFill(all - fixProvisionalDoubleThreading);
|
||||
testSignAndSubmitSequence(all);
|
||||
testAccountInfo(all);
|
||||
testServerInfo(all);
|
||||
|
||||
@@ -164,8 +164,8 @@ class AccountTx_test : public beast::unit_test::suite
|
||||
(payment[jss::validated] == true) &&
|
||||
(payment[jss::ledger_index] == 3) &&
|
||||
(payment[jss::ledger_hash] ==
|
||||
"6A55EEBF003EB79C1BF0B8FF75CBF448161F60E3366808820"
|
||||
"06903B7CC077EF2") &&
|
||||
"6B1FECE09EE027F4D035A1C0DDE3562E527606AF97B57EF3B"
|
||||
"E259617D67C8F37") &&
|
||||
(payment[jss::close_time_iso] ==
|
||||
"2000-01-01T00:00:10Z");
|
||||
}
|
||||
|
||||
@@ -3306,7 +3306,8 @@ public:
|
||||
section.set("normal_consensus_increase_percent", "0");
|
||||
return cfg;
|
||||
}),
|
||||
supported_amendments() - featureXahauGenesis};
|
||||
supported_amendments() - featureXahauGenesis -
|
||||
fixProvisionalDoubleThreading};
|
||||
|
||||
Json::Value jv;
|
||||
jv[jss::ledger_index] = "current";
|
||||
|
||||
@@ -349,8 +349,8 @@ public:
|
||||
== "2000-01-01T00:00:10Z" &&
|
||||
jv[jss::validated] == true && //
|
||||
jv[jss::ledger_hash] ==
|
||||
"8730420CE799AD878693358C0C927A72407E4D032E951C854399890312"
|
||||
"116400" && //
|
||||
"3BD88B8E93BED46C0B1ACB2C46687DE29F19F9BB82DE6C3D8CC491D6AE"
|
||||
"DEE517" && //
|
||||
!jv[jss::inLedger] &&
|
||||
jv[jss::ledger_index] == 3 && //
|
||||
jv[jss::tx_json][jss::TransactionType] //
|
||||
|
||||
@@ -279,7 +279,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
||||
"TransactionType" : "AccountSet",
|
||||
"TxnSignature" : "3044022007B35E3B99460534FF6BC3A66FBBA03591C355CC38E38588968E87CCD01BE229022071A443026DE45041B55ABB1CC76812A87EA701E475BBB7E165513B4B242D3474",
|
||||
})",
|
||||
"9E73E138359D4EB17B7F51F5F910E27F7920E9C50713E6D805A8AA8A87F9A43B",
|
||||
"5E8D88365131CA2EA3E0ADAFFB4A927D16B3820744D60B1303A6D9398358693D",
|
||||
"2000-01-01T00:00:10Z");
|
||||
check_tx(
|
||||
env.closed()->seq(),
|
||||
@@ -293,7 +293,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
||||
"TransactionType" : "AccountSet",
|
||||
"TxnSignature" : "3045022100C8857FC0759A2AC0D2F320684691A66EAD252EAED9EF88C79791BC58BFCC9D860220421722286487DD0ED6BBA626CE6FCBDD14289F7F4726870C3465A4054C2702D7",
|
||||
})",
|
||||
"9E73E138359D4EB17B7F51F5F910E27F7920E9C50713E6D805A8AA8A87F9A43B",
|
||||
"5E8D88365131CA2EA3E0ADAFFB4A927D16B3820744D60B1303A6D9398358693D",
|
||||
"2000-01-01T00:00:10Z");
|
||||
|
||||
env.trust(A2["USD"](1000), A1);
|
||||
@@ -328,7 +328,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
||||
"TransactionType" : "Payment",
|
||||
"TxnSignature" : "3044022033D9EBF7F02950AF2F6B13C07AEE641C8FEBDD540A338FCB9027A965A4AED35B02206E4E227DCC226A3456C0FEF953449D21645A24EB63CA0BB7C5B62470147FD1D1",
|
||||
})",
|
||||
"C0B5ECAE7066D926A7C486F0AC6A234DDD8468A4021C8D851A6CA57CE0467CB5",
|
||||
"41DB005149858C9BE599AE98FE1DF5BD4E9E0265B53DD4792532E9296A0773B6",
|
||||
"2000-01-01T00:00:20Z");
|
||||
|
||||
check_tx(
|
||||
@@ -350,7 +350,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
||||
"TransactionType" : "Payment",
|
||||
"TxnSignature" : "30450221008A722B7F16EDB2348886E88ED4EC682AE9973CC1EE0FF37C93BB2CEC821D3EDF022059E464472031BA5E0D88A93E944B6A8B8DB3E1D5E5D1399A805F615789DB0BED",
|
||||
})",
|
||||
"C0B5ECAE7066D926A7C486F0AC6A234DDD8468A4021C8D851A6CA57CE0467CB5",
|
||||
"41DB005149858C9BE599AE98FE1DF5BD4E9E0265B53DD4792532E9296A0773B6",
|
||||
"2000-01-01T00:00:20Z");
|
||||
|
||||
env(offer(A2, XRP(100), A2["USD"](1)));
|
||||
@@ -379,7 +379,7 @@ class TransactionEntry_test : public beast::unit_test::suite
|
||||
"TransactionType" : "OfferCreate",
|
||||
"TxnSignature" : "304502210093FC93ACB77B4E3DE3315441BD010096734859080C1797AB735EB47EBD541BD102205020BB1A7C3B4141279EE4C287C13671E2450EA78914EFD0C6DB2A18344CD4F2",
|
||||
})",
|
||||
"EED1C1D4364BBA9955EA0D603B45273B4F2B8D089A428F8787EAD625551AD342",
|
||||
"87622B7BC61E8CD01E7C5FC5198CA5A255F8468983CE1E19245FEE3068D97968",
|
||||
"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_hash] ==
|
||||
"F7A5A71945502089F557ECACD71EAB0A0B4E99BED527C2BF2DCD40C52130ED"
|
||||
"BC");
|
||||
"34BBC578F3A4EB6FC6C192C38F99EADD512316B32FA60B425764F1F9602DBB"
|
||||
"00");
|
||||
}
|
||||
|
||||
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);
|
||||
BEAST_EXPECT(
|
||||
result[jss::result][jss::ledger_hash] ==
|
||||
"892EBD0B2801B921DC20E8AA82914F3C1DD54B1789E9CFB1966F717C01"
|
||||
"73B2C8");
|
||||
"6BE57FA882745536BF528B09E0BAD3F31FC7CDA0284DDD0E0B97540550"
|
||||
"FFBECF");
|
||||
BEAST_EXPECT(
|
||||
result[jss::result][jss::close_time_iso] ==
|
||||
"2000-01-01T00:00:10Z");
|
||||
|
||||
@@ -118,7 +118,8 @@ ApplyStateTable::generateTxMeta(
|
||||
std::optional<STAmount> const& deliver,
|
||||
std::vector<STObject> const& hookExecution,
|
||||
std::vector<STObject> const& hookEmission,
|
||||
beast::Journal j)
|
||||
beast::Journal j,
|
||||
bool isProvisional)
|
||||
{
|
||||
TxMeta meta(tx.getTransactionID(), to.seq());
|
||||
if (deliver)
|
||||
@@ -202,7 +203,7 @@ ApplyStateTable::generateTxMeta(
|
||||
|
||||
if (curNode->isThreadedType(to.rules())) // thread transaction to
|
||||
// node item modified
|
||||
threadItem(meta, curNode);
|
||||
threadItem(meta, curNode, to.rules());
|
||||
|
||||
STObject prevs(sfPreviousFields);
|
||||
for (auto const& obj : *origNode)
|
||||
@@ -238,7 +239,7 @@ ApplyStateTable::generateTxMeta(
|
||||
threadOwners(to, meta, curNode, newMod, j);
|
||||
|
||||
if (curNode->isThreadedType(to.rules())) // always thread to self
|
||||
threadItem(meta, curNode);
|
||||
threadItem(meta, curNode, to.rules());
|
||||
|
||||
STObject news(sfNewFields);
|
||||
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};
|
||||
}
|
||||
|
||||
@@ -305,6 +334,7 @@ ApplyStateTable::apply(
|
||||
JLOG(j.trace()) << "metadata " << meta.getJson(JsonOptions::none);
|
||||
|
||||
metadata = meta;
|
||||
// Metadata has been generated
|
||||
}
|
||||
|
||||
if (!isDryRun)
|
||||
@@ -572,9 +602,51 @@ ApplyStateTable::destroyXRP(XRPAmount const& fee)
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// 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
|
||||
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;
|
||||
LedgerIndex prevLgrID;
|
||||
|
||||
@@ -593,6 +665,11 @@ ApplyStateTable::threadItem(TxMeta& meta, std::shared_ptr<SLE> const& sle)
|
||||
"set");
|
||||
node.setFieldH256(sfPreviousTxnID, prevTxID);
|
||||
node.setFieldU32(sfPreviousTxnLgrSeq, prevLgrID);
|
||||
// Added PreviousTxnID to metadata
|
||||
}
|
||||
else
|
||||
{
|
||||
// PreviousTxnID already present in metadata
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
@@ -677,7 +754,7 @@ ApplyStateTable::threadTx(
|
||||
XRPL_ASSERT(
|
||||
sle->isThreadedType(base.rules()),
|
||||
"ripple::ApplyStateTable::threadTx : SLE is threaded");
|
||||
threadItem(meta, sle);
|
||||
threadItem(meta, sle, base.rules());
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <xrpld/ledger/RawView.h>
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/TxMeta.h>
|
||||
#include <xrpl/protocol/XRPAmount.h>
|
||||
@@ -53,6 +54,18 @@ private:
|
||||
items_t items_;
|
||||
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:
|
||||
ApplyStateTable() = default;
|
||||
ApplyStateTable(ApplyStateTable&&) = default;
|
||||
@@ -73,7 +86,8 @@ public:
|
||||
std::optional<STAmount> const& deliver,
|
||||
std::vector<STObject> const& hookExecution,
|
||||
std::vector<STObject> const& hookEmission,
|
||||
beast::Journal j);
|
||||
beast::Journal j,
|
||||
bool isProvisional = false);
|
||||
|
||||
std::optional<TxMeta>
|
||||
apply(
|
||||
@@ -139,8 +153,11 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
static void
|
||||
threadItem(TxMeta& meta, std::shared_ptr<SLE> const& to);
|
||||
void
|
||||
threadItem(
|
||||
TxMeta& meta,
|
||||
std::shared_ptr<SLE> const& to,
|
||||
Rules const& rules);
|
||||
|
||||
std::shared_ptr<SLE>
|
||||
getForMod(
|
||||
|
||||
@@ -47,8 +47,13 @@ ApplyViewImpl::generateProvisionalMeta(
|
||||
beast::Journal j)
|
||||
{
|
||||
auto [meta, _] = items_.generateTxMeta(
|
||||
to, tx, deliver_, hookExecution_, hookEmission_, j);
|
||||
|
||||
to,
|
||||
tx,
|
||||
deliver_,
|
||||
hookExecution_,
|
||||
hookEmission_,
|
||||
j,
|
||||
true); // isProvisional = true
|
||||
return meta;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user