fix(export): return duplicate for live shadow tickets

A second pending export for the same shadow-ticket key is a ledger-object conflict, not an internal failure. Return tecDUPLICATE and cover the live-latch case in Export_test.
This commit is contained in:
Nicholas Dudfield
2026-06-26 19:14:10 +07:00
parent c74295e51d
commit ed3c0f07a7
2 changed files with 10 additions and 6 deletions

View File

@@ -1053,7 +1053,9 @@ struct Export_test : public beast::unit_test::suite
env.fund(XRP(10000), alice, carol);
env.close();
auto submitClosedExport = [&](std::uint32_t ticketSeq, TER expected) {
auto submitClosedExport = [&](std::uint32_t ticketSeq,
TER expected,
bool expectShadow) {
auto const seq = env.current()->seq();
auto innerObj = buildExportedPayment(
alice.id(), carol.id(), seq + 1, seq + 50, ticketSeq);
@@ -1072,14 +1074,16 @@ struct Export_test : public beast::unit_test::suite
auto const shadow =
env.le(keylet::shadowTicket(alice.id(), ticketSeq));
BEAST_EXPECT((expected == tesSUCCESS) == static_cast<bool>(shadow));
BEAST_EXPECT(expectShadow == static_cast<bool>(shadow));
env.close();
};
for (std::uint32_t i = 1; i <= ExportLimits::maxPendingExports; ++i)
submitClosedExport(i, tesSUCCESS);
submitClosedExport(i, tesSUCCESS, true);
submitClosedExport(ExportLimits::maxPendingExports + 1, tecDIR_FULL);
submitClosedExport(1, tecDUPLICATE, true);
submitClosedExport(
ExportLimits::maxPendingExports + 1, tecDIR_FULL, false);
}
void

View File

@@ -180,7 +180,7 @@ validateTicketSequence(STTx const& stx, beast::Journal j)
/// @param stx The exported transaction (checked for sfTicketSequence)
/// @param txnId Hash of the exported transaction
/// @param j Journal for logging
/// @return tesSUCCESS, tecDIR_FULL, or tefINTERNAL
/// @return tesSUCCESS, tecDUPLICATE, tecDIR_FULL, or tefINTERNAL
inline TER
createShadowTicket(
ApplyView& view,
@@ -211,7 +211,7 @@ createShadowTicket(
{
JLOG(j.warn()) << "ExportLedgerOps: shadow ticket already exists for "
<< account << " seq=" << ticketSeq;
return tefINTERNAL;
return tecDUPLICATE;
}
auto const pending = shadowTicketCount(view, account);