mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-29 15:37:46 +00:00
318 lines
13 KiB
C++
318 lines
13 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
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 <ripple/protocol/jss.h>
|
|
#include <test/jtx.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({});
|
|
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
|