mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-27 09:00:09 +00:00
refactor(export): key lifecycle control by origin
This commit is contained in:
@@ -350,7 +350,7 @@ xport(
|
||||
uint32_t read_len);
|
||||
|
||||
extern int64_t
|
||||
xport_cancel(uint32_t ticket_seq);
|
||||
xport_cancel(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
|
||||
|
||||
/*
|
||||
Consensus entropy APIs.
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#define sfHookExportCount ((1U << 16U) + 98U)
|
||||
#define sfEntropyCount ((1U << 16U) + 99U)
|
||||
#define sfEntropyDenominator ((1U << 16U) + 100U)
|
||||
#define sfExportCount ((1U << 16U) + 101U)
|
||||
#define sfNetworkID ((2U << 16U) + 1U)
|
||||
#define sfFlags ((2U << 16U) + 2U)
|
||||
#define sfSourceTag ((2U << 16U) + 3U)
|
||||
@@ -85,7 +86,6 @@
|
||||
#define sfRewardTime ((2U << 16U) + 98U)
|
||||
#define sfRewardLgrFirst ((2U << 16U) + 99U)
|
||||
#define sfRewardLgrLast ((2U << 16U) + 100U)
|
||||
#define sfCancelTicketSequence ((2U << 16U) + 101U)
|
||||
#define sfIndexNext ((3U << 16U) + 1U)
|
||||
#define sfIndexPrevious ((3U << 16U) + 2U)
|
||||
#define sfBookNode ((3U << 16U) + 3U)
|
||||
@@ -111,6 +111,7 @@
|
||||
#define sfMPTAmount ((3U << 16U) + 26U)
|
||||
#define sfIssuerNode ((3U << 16U) + 27U)
|
||||
#define sfSubjectNode ((3U << 16U) + 28U)
|
||||
#define sfExportNode ((3U << 16U) + 29U)
|
||||
#define sfTouchCount ((3U << 16U) + 97U)
|
||||
#define sfAccountIndex ((3U << 16U) + 98U)
|
||||
#define sfAccountCount ((3U << 16U) + 99U)
|
||||
@@ -159,6 +160,7 @@
|
||||
#define sfURITokenID ((5U << 16U) + 36U)
|
||||
#define sfDomainID ((5U << 16U) + 37U)
|
||||
#define sfExportSignatureHash ((5U << 16U) + 38U)
|
||||
#define sfExportUniverseHash ((5U << 16U) + 39U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
#define sfCron ((5U << 16U) + 95U)
|
||||
@@ -229,6 +231,7 @@
|
||||
#define sfMPTokenMetadata ((7U << 16U) + 31U)
|
||||
#define sfCredentialType ((7U << 16U) + 32U)
|
||||
#define sfEntropyContributors ((7U << 16U) + 33U)
|
||||
#define sfExportCommittee ((7U << 16U) + 34U)
|
||||
#define sfHookName ((7U << 16U) + 97U)
|
||||
#define sfRemarkValue ((7U << 16U) + 98U)
|
||||
#define sfRemarkName ((7U << 16U) + 99U)
|
||||
@@ -281,6 +284,7 @@
|
||||
#define sfEmitDetails ((14U << 16U) + 13U)
|
||||
#define sfHook ((14U << 16U) + 14U)
|
||||
#define sfSigner ((14U << 16U) + 16U)
|
||||
#define sfExportSigner ((14U << 16U) + 17U)
|
||||
#define sfMajority ((14U << 16U) + 18U)
|
||||
#define sfDisabledValidator ((14U << 16U) + 19U)
|
||||
#define sfEmittedTxn ((14U << 16U) + 20U)
|
||||
@@ -324,6 +328,7 @@
|
||||
#define sfHookGrants ((15U << 16U) + 20U)
|
||||
#define sfXChainClaimAttestations ((15U << 16U) + 21U)
|
||||
#define sfXChainCreateAccountAttestations ((15U << 16U) + 22U)
|
||||
#define sfExportSigners ((15U << 16U) + 23U)
|
||||
#define sfPriceDataSeries ((15U << 16U) + 24U)
|
||||
#define sfAuthAccounts ((15U << 16U) + 25U)
|
||||
#define sfAuthorizeCredentials ((15U << 16U) + 26U)
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#define sfHookExportCount ((1U << 16U) + 98U)
|
||||
#define sfEntropyCount ((1U << 16U) + 99U)
|
||||
#define sfEntropyDenominator ((1U << 16U) + 100U)
|
||||
#define sfExportCount ((1U << 16U) + 101U)
|
||||
#define sfNetworkID ((2U << 16U) + 1U)
|
||||
#define sfFlags ((2U << 16U) + 2U)
|
||||
#define sfSourceTag ((2U << 16U) + 3U)
|
||||
@@ -85,7 +86,6 @@
|
||||
#define sfRewardTime ((2U << 16U) + 98U)
|
||||
#define sfRewardLgrFirst ((2U << 16U) + 99U)
|
||||
#define sfRewardLgrLast ((2U << 16U) + 100U)
|
||||
#define sfCancelTicketSequence ((2U << 16U) + 101U)
|
||||
#define sfIndexNext ((3U << 16U) + 1U)
|
||||
#define sfIndexPrevious ((3U << 16U) + 2U)
|
||||
#define sfBookNode ((3U << 16U) + 3U)
|
||||
@@ -111,6 +111,7 @@
|
||||
#define sfMPTAmount ((3U << 16U) + 26U)
|
||||
#define sfIssuerNode ((3U << 16U) + 27U)
|
||||
#define sfSubjectNode ((3U << 16U) + 28U)
|
||||
#define sfExportNode ((3U << 16U) + 29U)
|
||||
#define sfTouchCount ((3U << 16U) + 97U)
|
||||
#define sfAccountIndex ((3U << 16U) + 98U)
|
||||
#define sfAccountCount ((3U << 16U) + 99U)
|
||||
@@ -159,6 +160,7 @@
|
||||
#define sfURITokenID ((5U << 16U) + 36U)
|
||||
#define sfDomainID ((5U << 16U) + 37U)
|
||||
#define sfExportSignatureHash ((5U << 16U) + 38U)
|
||||
#define sfExportUniverseHash ((5U << 16U) + 39U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
#define sfCron ((5U << 16U) + 95U)
|
||||
@@ -229,6 +231,7 @@
|
||||
#define sfMPTokenMetadata ((7U << 16U) + 31U)
|
||||
#define sfCredentialType ((7U << 16U) + 32U)
|
||||
#define sfEntropyContributors ((7U << 16U) + 33U)
|
||||
#define sfExportCommittee ((7U << 16U) + 34U)
|
||||
#define sfHookName ((7U << 16U) + 97U)
|
||||
#define sfRemarkValue ((7U << 16U) + 98U)
|
||||
#define sfRemarkName ((7U << 16U) + 99U)
|
||||
@@ -281,6 +284,7 @@
|
||||
#define sfEmitDetails ((14U << 16U) + 13U)
|
||||
#define sfHook ((14U << 16U) + 14U)
|
||||
#define sfSigner ((14U << 16U) + 16U)
|
||||
#define sfExportSigner ((14U << 16U) + 17U)
|
||||
#define sfMajority ((14U << 16U) + 18U)
|
||||
#define sfDisabledValidator ((14U << 16U) + 19U)
|
||||
#define sfEmittedTxn ((14U << 16U) + 20U)
|
||||
@@ -324,6 +328,7 @@
|
||||
#define sfHookGrants ((15U << 16U) + 20U)
|
||||
#define sfXChainClaimAttestations ((15U << 16U) + 21U)
|
||||
#define sfXChainCreateAccountAttestations ((15U << 16U) + 22U)
|
||||
#define sfExportSigners ((15U << 16U) + 23U)
|
||||
#define sfPriceDataSeries ((15U << 16U) + 24U)
|
||||
#define sfAuthAccounts ((15U << 16U) + 25U)
|
||||
#define sfAuthorizeCredentials ((15U << 16U) + 26U)
|
||||
|
||||
@@ -116,6 +116,11 @@ enum BridgeModifyFlags : uint32_t {
|
||||
tfClearAccountCreateAmount = 0x00010000,
|
||||
};
|
||||
|
||||
enum ExportFlags : uint32_t {
|
||||
// Lifecycle control: erase the named latch and forfeit any later callback.
|
||||
tfExportEraseLatch = 0x00010000,
|
||||
};
|
||||
|
||||
enum ConsensusEntropyFlags : uint32_t {
|
||||
tfEntropyCommit = 0x00000001, // entry is a commitment in commitSet
|
||||
tfEntropyReveal = 0x00000002, // entry is a reveal in entropySet
|
||||
|
||||
@@ -383,9 +383,9 @@ HOOK_API_DEFINITION(
|
||||
int64_t, xport, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
featureExport)
|
||||
|
||||
// int64_t xport_cancel(uint32_t ticket_seq);
|
||||
// int64_t xport_cancel(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xport_cancel, (uint32_t),
|
||||
int64_t, xport_cancel, (uint32_t, uint32_t, uint32_t),
|
||||
featureExport)
|
||||
|
||||
// int64_t dice(uint32_t sides, uint32_t min_tier, uint32_t min_count);
|
||||
|
||||
@@ -275,6 +275,14 @@ enum BridgeModifyFlags : uint32_t {
|
||||
};
|
||||
constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount);
|
||||
|
||||
// Export flags:
|
||||
enum ExportFlags : uint32_t {
|
||||
// Lifecycle control: erase the named latch and forfeit any later callback.
|
||||
tfExportEraseLatch = 0x00010000,
|
||||
};
|
||||
constexpr std::uint32_t tfExportMask =
|
||||
~(tfUniversal | tfExportEraseLatch);
|
||||
|
||||
// ConsensusEntropy flags (used on ttCONSENSUS_ENTROPY SHAMap entries):
|
||||
enum ConsensusEntropyFlags : uint32_t {
|
||||
tfEntropyCommit = 0x00000001, // entry is a commitment in commitSet
|
||||
|
||||
@@ -129,7 +129,6 @@ TYPED_SFIELD(sfImportSequence, UINT32, 97)
|
||||
TYPED_SFIELD(sfRewardTime, UINT32, 98)
|
||||
TYPED_SFIELD(sfRewardLgrFirst, UINT32, 99)
|
||||
TYPED_SFIELD(sfRewardLgrLast, UINT32, 100)
|
||||
TYPED_SFIELD(sfCancelTicketSequence, UINT32, 101)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
|
||||
@@ -503,11 +503,11 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
|
||||
//@@start export-transaction-types
|
||||
/* User-submittable export: creates a cross-chain transaction for
|
||||
post-validation committee signing and a durable callback latch.
|
||||
Also supports Export latch cancellation via sfCancelTicketSequence.
|
||||
At least one of sfExportedTxn or sfCancelTicketSequence must be present. */
|
||||
A lifecycle-control form names an existing Export by its source transaction
|
||||
hash. Exactly one of sfExportedTxn or sfTransactionHash must be present. */
|
||||
TRANSACTION(ttEXPORT, 91, Export, ({
|
||||
{sfExportedTxn, soeOPTIONAL},
|
||||
{sfCancelTicketSequence, soeOPTIONAL},
|
||||
{sfTransactionHash, soeOPTIONAL},
|
||||
{sfExportUniverseHash, soeOPTIONAL},
|
||||
{sfExportCommittee, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
@@ -234,9 +234,9 @@ struct ExportLatch_test : beast::unit_test::suite
|
||||
}
|
||||
|
||||
void
|
||||
testCanceledLatchAcceptsBothFactOrders()
|
||||
testRetainedLatchAcceptsBothFactOrders()
|
||||
{
|
||||
testcase("canceled latch accepts XPOP and witness in either order");
|
||||
testcase("retained latch accepts XPOP and witness in either order");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
@@ -261,8 +261,13 @@ struct ExportLatch_test : beast::unit_test::suite
|
||||
auto const xpopFirstKey = keylet::unchecked(xpopFirst->key());
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, xpopFirst, j)));
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::cancelExportLatch(
|
||||
sb, sb, alice.id(), xpopFirst->getFieldU32(sfTicketSequence), j)));
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
|
||||
sb,
|
||||
sb,
|
||||
alice.id(),
|
||||
xpopFirst->getFieldH256(sfTransactionHash),
|
||||
false,
|
||||
j)));
|
||||
|
||||
auto const canceled = sb.read(xpopFirstKey);
|
||||
BEAST_EXPECT(canceled);
|
||||
@@ -308,11 +313,12 @@ struct ExportLatch_test : beast::unit_test::suite
|
||||
auto const witnessFirstKey = keylet::unchecked(witnessFirst->key());
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::insertPendingExportLatch(
|
||||
sb, sb, witnessFirst, j)));
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::cancelExportLatch(
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
|
||||
sb,
|
||||
sb,
|
||||
alice.id(),
|
||||
witnessFirst->getFieldU32(sfTicketSequence),
|
||||
witnessFirst->getFieldH256(sfTransactionHash),
|
||||
false,
|
||||
j)));
|
||||
|
||||
// The accepted witness may already have been materialized before
|
||||
@@ -350,12 +356,97 @@ struct ExportLatch_test : beast::unit_test::suite
|
||||
BEAST_EXPECT(ExportLedgerOps::exportLatchCount(*rootAfterXpop) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testExplicitEraseAndExpiryRetention()
|
||||
{
|
||||
testcase("explicit erase and expiry retention");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
Env env{*this};
|
||||
env.fund(XRP(10'000), alice);
|
||||
env.close();
|
||||
|
||||
beast::Journal j{beast::Journal::getNullSink()};
|
||||
Sandbox sb{env.closed().get(), tapNONE};
|
||||
auto const baselineAccount = sb.read(keylet::account(alice.id()));
|
||||
auto const baselinePending = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(baselineAccount);
|
||||
if (!baselineAccount)
|
||||
return;
|
||||
auto const baselineExportCount =
|
||||
ExportLedgerOps::exportLatchCount(*baselineAccount);
|
||||
auto const baselineOwnerCount =
|
||||
baselineAccount->getFieldU32(sfOwnerCount);
|
||||
auto const baselinePendingCount = baselinePending
|
||||
? ExportLedgerOps::exportLatchCount(*baselinePending)
|
||||
: std::uint16_t{0};
|
||||
|
||||
auto erased = makeLatch(alice.id(), 0);
|
||||
auto const erasedOrigin = erased->getFieldH256(sfTransactionHash);
|
||||
auto const erasedKey = keylet::exportLatch(alice.id(), erasedOrigin);
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, erased, j)));
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
|
||||
sb, sb, alice.id(), erasedOrigin, true, j)));
|
||||
BEAST_EXPECT(!sb.exists(erasedKey));
|
||||
|
||||
auto const afterEraseAccount = sb.read(keylet::account(alice.id()));
|
||||
auto const afterErasePending = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(afterEraseAccount);
|
||||
BEAST_EXPECT(afterErasePending);
|
||||
if (!afterEraseAccount || !afterErasePending)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*afterEraseAccount) ==
|
||||
baselineExportCount);
|
||||
BEAST_EXPECT(
|
||||
afterEraseAccount->getFieldU32(sfOwnerCount) == baselineOwnerCount);
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*afterErasePending) ==
|
||||
baselinePendingCount);
|
||||
|
||||
auto expired = makeLatch(alice.id(), 1);
|
||||
expired->setFieldU32(sfLastLedgerSequence, 100);
|
||||
auto const expiredOrigin = expired->getFieldH256(sfTransactionHash);
|
||||
auto const expiredKey = keylet::exportLatch(alice.id(), expiredOrigin);
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::insertPendingExportLatch(sb, sb, expired, j)));
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
ExportLedgerOps::pruneExpiredExportLatches(sb, sb, 101, j)));
|
||||
|
||||
auto const retained = sb.read(expiredKey);
|
||||
auto const afterExpiryAccount = sb.read(keylet::account(alice.id()));
|
||||
auto const afterExpiryPending = sb.read(keylet::pendingExports());
|
||||
BEAST_EXPECT(retained);
|
||||
BEAST_EXPECT(afterExpiryAccount);
|
||||
BEAST_EXPECT(afterExpiryPending);
|
||||
if (!retained || !afterExpiryAccount || !afterExpiryPending)
|
||||
return;
|
||||
BEAST_EXPECT(!retained->isFieldPresent(sfExportNode));
|
||||
BEAST_EXPECT((retained->getFieldU32(sfFlags) & lsfExportCanceled) != 0);
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*afterExpiryAccount) ==
|
||||
baselineExportCount + 1);
|
||||
BEAST_EXPECT(
|
||||
afterExpiryAccount->getFieldU32(sfOwnerCount) ==
|
||||
baselineOwnerCount + 1);
|
||||
BEAST_EXPECT(
|
||||
ExportLedgerOps::exportLatchCount(*afterExpiryPending) ==
|
||||
baselinePendingCount);
|
||||
|
||||
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
|
||||
sb, sb, alice.id(), expiredOrigin, true, j)));
|
||||
BEAST_EXPECT(!sb.exists(expiredKey));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDirectoryLifecycle();
|
||||
testWitnessedLatchReleasesGlobalPendingCap();
|
||||
testCanceledLatchAcceptsBothFactOrders();
|
||||
testRetainedLatchAcceptsBothFactOrders();
|
||||
testExplicitEraseAndExpiryRetention();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1942,9 +1942,9 @@ struct Export_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
void
|
||||
testCancelExportLatchViaTxn(FeatureBitset features)
|
||||
testControlExportLatchViaTxn(FeatureBitset features)
|
||||
{
|
||||
testcase("ttEXPORT cancels Export latch via sfCancelTicketSequence");
|
||||
testcase("ttEXPORT controls Export latch via origin W");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
@@ -1961,13 +1961,22 @@ struct Export_test : public beast::unit_test::suite
|
||||
return;
|
||||
seedUNLReportLedger(env, {valKeys.keys->masterPublicKey});
|
||||
|
||||
// Cancel non-existent ticket → tecNO_ENTRY
|
||||
Json::Value jvCancel;
|
||||
jvCancel[jss::TransactionType] = jss::Export;
|
||||
jvCancel[jss::Account] = alice.human();
|
||||
jvCancel[sfCancelTicketSequence.jsonName] = 42;
|
||||
// Control of a non-existent W returns tecNO_ENTRY.
|
||||
Json::Value jvControl;
|
||||
jvControl[jss::TransactionType] = jss::Export;
|
||||
jvControl[jss::Account] = alice.human();
|
||||
jvControl[sfTransactionHash.jsonName] = to_string(uint256{42});
|
||||
|
||||
env(jvCancel, fee(XRP(1)), ter(tecNO_ENTRY));
|
||||
env(jvControl, fee(XRP(1)), ter(tecNO_ENTRY));
|
||||
|
||||
auto zeroControl = jvControl;
|
||||
zeroControl[sfTransactionHash.jsonName] = to_string(uint256{});
|
||||
env(zeroControl, fee(XRP(1)), ter(temMALFORMED));
|
||||
|
||||
Json::Value emptyControl;
|
||||
emptyControl[jss::TransactionType] = jss::Export;
|
||||
emptyControl[jss::Account] = alice.human();
|
||||
env(emptyControl, fee(XRP(1)), ter(temMALFORMED));
|
||||
env.close();
|
||||
|
||||
std::uint32_t constexpr ticketSeq = 42;
|
||||
@@ -1986,6 +1995,18 @@ struct Export_test : public beast::unit_test::suite
|
||||
jvExport[sfExportedTxn.jsonName] = innerObj.getJson(JsonOptions::none);
|
||||
bindExportAuthority(env, jvExport);
|
||||
|
||||
auto mixed = jvExport;
|
||||
mixed[sfTransactionHash.jsonName] = to_string(uint256{42});
|
||||
env(mixed, fee(XRP(1)), ter(temMALFORMED));
|
||||
|
||||
auto eraseCreation = jvExport;
|
||||
eraseCreation[jss::Flags] = tfExportEraseLatch;
|
||||
env(eraseCreation, fee(XRP(1)), ter(temMALFORMED));
|
||||
|
||||
auto unknownFlag = jvControl;
|
||||
unknownFlag[jss::Flags] = 0x00020000;
|
||||
env(unknownFlag, fee(XRP(1)), ter(temINVALID_FLAG));
|
||||
|
||||
env(jvExport, fee(XRP(1)), ter(tesSUCCESS));
|
||||
auto const origin = env.tx()->getTransactionID();
|
||||
auto const exportMeta = env.meta();
|
||||
@@ -2011,12 +2032,12 @@ struct Export_test : public beast::unit_test::suite
|
||||
pendingBeforeCancel->getFieldU16(sfExportCount);
|
||||
BEAST_EXPECT(pendingExportCount > 0);
|
||||
|
||||
Json::Value jvCancelExisting;
|
||||
jvCancelExisting[jss::TransactionType] = jss::Export;
|
||||
jvCancelExisting[jss::Account] = alice.human();
|
||||
jvCancelExisting[sfCancelTicketSequence.jsonName] = ticketSeq;
|
||||
Json::Value jvRetain;
|
||||
jvRetain[jss::TransactionType] = jss::Export;
|
||||
jvRetain[jss::Account] = alice.human();
|
||||
jvRetain[sfTransactionHash.jsonName] = to_string(origin);
|
||||
|
||||
env(jvCancelExisting, fee(XRP(1)), ter(tesSUCCESS));
|
||||
env(jvRetain, fee(XRP(1)), ter(tesSUCCESS));
|
||||
auto const cancelMeta = env.meta();
|
||||
BEAST_EXPECT(cancelMeta);
|
||||
BEAST_EXPECT(
|
||||
@@ -2049,6 +2070,26 @@ struct Export_test : public beast::unit_test::suite
|
||||
pendingAfterCancel->getFieldU16(sfExportCount) ==
|
||||
pendingExportCount - 1);
|
||||
|
||||
auto jvErase = jvRetain;
|
||||
jvErase[jss::Flags] = tfExportEraseLatch;
|
||||
env(jvErase, fee(XRP(1)), ter(tesSUCCESS));
|
||||
|
||||
auto const accountAfterErase = env.le(keylet::account(alice.id()));
|
||||
auto const pendingAfterErase = env.le(keylet::pendingExports());
|
||||
BEAST_EXPECT(!env.le(latchKey));
|
||||
BEAST_EXPECT(accountAfterErase);
|
||||
BEAST_EXPECT(pendingAfterErase);
|
||||
if (!accountAfterErase || !pendingAfterErase)
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
accountAfterErase->getFieldU16(sfExportCount) ==
|
||||
accountExportCount - 1);
|
||||
BEAST_EXPECT(
|
||||
accountAfterErase->getFieldU32(sfOwnerCount) == ownerCount - 1);
|
||||
BEAST_EXPECT(
|
||||
pendingAfterErase->getFieldU16(sfExportCount) ==
|
||||
pendingExportCount - 1);
|
||||
|
||||
env.close();
|
||||
}
|
||||
|
||||
@@ -2290,7 +2331,7 @@ struct Export_test : public beast::unit_test::suite
|
||||
Json::Value cancel;
|
||||
cancel[jss::TransactionType] = jss::Export;
|
||||
cancel[jss::Account] = alice.human();
|
||||
cancel[sfCancelTicketSequence.jsonName] = callback.ticketSeq;
|
||||
cancel[sfTransactionHash.jsonName] = to_string(callback.originTxn);
|
||||
xahau(cancel, fee(XRP(1)), ter(tesSUCCESS));
|
||||
xahau.close();
|
||||
|
||||
@@ -2406,7 +2447,7 @@ struct Export_test : public beast::unit_test::suite
|
||||
testOpenLedgerExportLimit(allWithExport);
|
||||
testExportLatchLimit(allWithExport);
|
||||
testExportLatchLifecycle(allWithExport);
|
||||
testCancelExportLatchViaTxn(allWithExport);
|
||||
testControlExportLatchViaTxn(allWithExport);
|
||||
testExportRejectsNoTicketSequence(allWithExport);
|
||||
testExportRejectsMissingLastLedgerSequence(allWithExport);
|
||||
testExportRejectsSignedInnerTransaction(allWithExport);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <xrpl/beast/unit_test/suite.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/ExportOriginMemo.h>
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -1201,12 +1202,22 @@ public:
|
||||
|
||||
std::uint32_t const importingTicket = 7;
|
||||
std::uint32_t const otherTicket = 8;
|
||||
uint256 const importingOrigin{1};
|
||||
uint256 const otherOrigin{2};
|
||||
auto importingTx =
|
||||
makeExportedPayment(alice.id(), bob.id(), importingTicket);
|
||||
auto otherTx = makeExportedPayment(alice.id(), bob.id(), otherTicket);
|
||||
auto const importingRelease = ExportOriginMemo::releaseForm(
|
||||
importingTx,
|
||||
ExportOriginMemo::Origin{21337, 0, importingOrigin},
|
||||
ExportOriginMemo::Anchor{1, uint256{99}});
|
||||
BEAST_EXPECT(importingRelease);
|
||||
if (!importingRelease)
|
||||
return;
|
||||
|
||||
auto xpopJson = import::loadXpop(ImportTCAccountSet::w_seed);
|
||||
xpopJson[jss::transaction][jss::blob] = strHex(serialize(importingTx));
|
||||
xpopJson[jss::transaction][jss::blob] =
|
||||
strHex(serialize(importingRelease.value()));
|
||||
std::string const xpopStr = Json::FastWriter().write(xpopJson);
|
||||
STTx importTx = STTx(ttIMPORT, [&](STObject& obj) {
|
||||
obj.setAccountID(sfAccount, alice.id());
|
||||
@@ -1232,8 +1243,6 @@ public:
|
||||
return ExportLedgerOps::insertPendingExportLatch(
|
||||
ctx.view(), ctx.rawView(), latch, env.journal);
|
||||
};
|
||||
uint256 const importingOrigin{1};
|
||||
uint256 const otherOrigin{2};
|
||||
BEAST_EXPECT(
|
||||
isTesSuccess(insertLatch(applyCtx, importingTx, importingOrigin)));
|
||||
BEAST_EXPECT(isTesSuccess(insertLatch(applyCtx, otherTx, otherOrigin)));
|
||||
@@ -1242,13 +1251,17 @@ public:
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
auto const blocked = api.xport_cancel(importingTicket);
|
||||
auto const blocked = api.xport_cancel(importingOrigin, 0);
|
||||
BEAST_EXPECT(!blocked.has_value());
|
||||
BEAST_EXPECT(blocked.error() == PREREQUISITE_NOT_MET);
|
||||
BEAST_EXPECT(hookCtx.applyCtx.view().exists(
|
||||
keylet::exportLatch(alice.id(), importingOrigin)));
|
||||
|
||||
auto const cancelled = api.xport_cancel(otherTicket);
|
||||
auto const invalidFlags = api.xport_cancel(otherOrigin, 0x00020000);
|
||||
BEAST_EXPECT(!invalidFlags.has_value());
|
||||
BEAST_EXPECT(invalidFlags.error() == INVALID_ARGUMENT);
|
||||
|
||||
auto const cancelled = api.xport_cancel(otherOrigin, 0);
|
||||
BEAST_EXPECT(cancelled.has_value());
|
||||
auto const canceledLatch = hookCtx.applyCtx.view().read(
|
||||
keylet::exportLatch(alice.id(), otherOrigin));
|
||||
@@ -1259,6 +1272,11 @@ public:
|
||||
BEAST_EXPECT(
|
||||
canceledLatch && !canceledLatch->isFieldPresent(sfExportNode));
|
||||
|
||||
auto const erased = api.xport_cancel(otherOrigin, tfExportEraseLatch);
|
||||
BEAST_EXPECT(erased.has_value());
|
||||
BEAST_EXPECT(!hookCtx.applyCtx.view().exists(
|
||||
keylet::exportLatch(alice.id(), otherOrigin)));
|
||||
|
||||
auto exportTx = makeExportWrapper(alice.id(), importingTx);
|
||||
OpenView exportOv{*env.current()};
|
||||
ApplyContext exportApplyCtx =
|
||||
@@ -1266,7 +1284,7 @@ public:
|
||||
std::uint32_t const exportOtherTicket = 9;
|
||||
auto exportOtherTx =
|
||||
makeExportedPayment(alice.id(), bob.id(), exportOtherTicket);
|
||||
uint256 const exportImportingOrigin{3};
|
||||
auto const exportImportingOrigin = exportTx.getTransactionID();
|
||||
uint256 const exportOtherOrigin{4};
|
||||
BEAST_EXPECT(isTesSuccess(
|
||||
insertLatch(exportApplyCtx, importingTx, exportImportingOrigin)));
|
||||
@@ -1276,13 +1294,15 @@ public:
|
||||
makeStubHookContext(exportApplyCtx, alice.id(), alice.id(), {});
|
||||
auto& exportApi = exportHookCtx.api();
|
||||
|
||||
auto const exportBlocked = exportApi.xport_cancel(importingTicket);
|
||||
auto const exportBlocked =
|
||||
exportApi.xport_cancel(exportImportingOrigin, 0);
|
||||
BEAST_EXPECT(!exportBlocked.has_value());
|
||||
BEAST_EXPECT(exportBlocked.error() == PREREQUISITE_NOT_MET);
|
||||
BEAST_EXPECT(exportHookCtx.applyCtx.view().exists(
|
||||
keylet::exportLatch(alice.id(), exportImportingOrigin)));
|
||||
|
||||
auto const exportCancelled = exportApi.xport_cancel(exportOtherTicket);
|
||||
auto const exportCancelled =
|
||||
exportApi.xport_cancel(exportOtherOrigin, 0);
|
||||
BEAST_EXPECT(exportCancelled.has_value());
|
||||
auto const canceledExportLatch = exportHookCtx.applyCtx.view().read(
|
||||
keylet::exportLatch(alice.id(), exportOtherOrigin));
|
||||
|
||||
@@ -571,15 +571,16 @@ later.
|
||||
|
||||
Export-latch cancellation is non-revoking. While publication is pending it
|
||||
unlinks signing work and retains callback readiness because shares already
|
||||
published to peers cannot be withdrawn. Once the latch is non-pending, an
|
||||
explicit owner cleanup may erase it, release reserve, and knowingly accept
|
||||
later target execution without callback readiness. Symmetric witness+XPOP
|
||||
published to peers cannot be withdrawn. Lifecycle control names the exact
|
||||
source issuance `W`: no flag always unlinks and retains in every state, while
|
||||
`tfExportEraseLatch` explicitly erases that exact latch in any state, releases
|
||||
reserve, and knowingly accepts later target execution without callback
|
||||
readiness. Publication expiry follows the retain path. Symmetric witness+XPOP
|
||||
completion also erases. v1 has no automatic terminal-retirement clock,
|
||||
permanent tombstone, or paid-bump transition.
|
||||
|
||||
Implementation status: origin-keyed latch creation and symmetric witness/XPOP
|
||||
recording are present. Explicit terminal cleanup and expiry-unlink behavior are
|
||||
ratified policy still tracked for implementation.
|
||||
Implementation status: origin-keyed creation, W-only lifecycle control,
|
||||
expiry-unlink, and symmetric witness/XPOP recording are present.
|
||||
|
||||
This is intentionally leaner than XPOP. XPOP carries its own UNL and manifest
|
||||
bundle so it can be independently verified as an external proof. Export witnesses
|
||||
|
||||
@@ -105,15 +105,18 @@ and ticket-keyed latch helper are not supported v1 compatibility paths.
|
||||
|
||||
Witness and XPOP are independent monotonic facts. Whichever arrives second
|
||||
symmetrically erases the latch and releases reserve. Cancellation while
|
||||
publication is pending is non-revoking: it unlinks work and retains callback
|
||||
readiness because already-public signatures cannot be withdrawn. Once a latch
|
||||
is non-pending, an explicit owner cleanup may erase it and knowingly forfeit a
|
||||
later callback. v1 has neither an automatic terminal-retirement clock nor a
|
||||
permanent tombstone graveyard.
|
||||
publication is pending is non-revoking: a lifecycle-control `ttEXPORT` names
|
||||
the exact source issuance `W` in `sfTransactionHash`, unlinks work, and retains
|
||||
callback readiness because already-public signatures cannot be withdrawn. The
|
||||
same bytes retain the latch in every state. `tfExportEraseLatch` is a separate,
|
||||
explicit election that erases that exact latch in any state, releases reserve,
|
||||
and knowingly forfeits a later callback. Publication expiry follows the
|
||||
non-revoking retain path. The undeployed Hook API uses the same W-plus-flags
|
||||
contract. v1 has neither an automatic terminal-retirement clock nor a permanent
|
||||
tombstone graveyard.
|
||||
|
||||
Implementation status: origin-keyed latch creation and symmetric witness/XPOP
|
||||
recording are present. Explicit terminal cleanup and expiry-unlink behavior are
|
||||
ratified policy still tracked for implementation.
|
||||
Implementation status: origin-keyed creation, W-only lifecycle control,
|
||||
expiry-unlink, and symmetric witness/XPOP recording are present.
|
||||
|
||||
**INV-8 — Export signatures are public capabilities.**
|
||||
Proposal-carried signature shares may be observed, assembled, and submitted as
|
||||
|
||||
@@ -377,7 +377,7 @@ public:
|
||||
xport(Slice const& txBlob) const;
|
||||
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
xport_cancel(uint32_t ticketSeq) const;
|
||||
xport_cancel(uint256 const& origin, uint32_t flags) const;
|
||||
|
||||
/// float APIs
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
|
||||
#include <xrpld/app/tx/detail/Import.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/ExportOriginMemo.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
|
||||
namespace hook {
|
||||
|
||||
@@ -1065,52 +1067,53 @@ HookAPI::xport(Slice const& txBlob) const
|
||||
}
|
||||
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
HookAPI::xport_cancel(uint32_t ticketSeq) const
|
||||
HookAPI::xport_cancel(uint256 const& origin, uint32_t flags) const
|
||||
{
|
||||
if (origin.isZero() || (flags != 0 && flags != tfExportEraseLatch))
|
||||
return Unexpected(INVALID_ARGUMENT);
|
||||
|
||||
auto& app = hookCtx.applyCtx.app;
|
||||
auto j = app.journal("View");
|
||||
auto const& currentTx = hookCtx.applyCtx.tx;
|
||||
auto const& account = hookCtx.result.account;
|
||||
|
||||
auto matchesTicket = [&](STTx const& innerTx) {
|
||||
return innerTx.isFieldPresent(sfTicketSequence) &&
|
||||
innerTx.getAccountID(sfAccount) == account &&
|
||||
innerTx.getFieldU32(sfTicketSequence) == ticketSeq;
|
||||
};
|
||||
|
||||
if (currentTx.getTxnType() == ttIMPORT)
|
||||
{
|
||||
auto const [innerTx, meta] = Import::getInnerTxn(currentTx, j);
|
||||
if (innerTx && matchesTicket(*innerTx))
|
||||
if (innerTx && innerTx->isFieldPresent(sfAccount) &&
|
||||
innerTx->getAccountID(sfAccount) == account)
|
||||
{
|
||||
// Import consumes this callback latch after strong hooks finish.
|
||||
// Letting the hook pre-cancel it would make the Import fail after
|
||||
// hook state already finalized.
|
||||
return Unexpected(PREREQUISITE_NOT_MET);
|
||||
auto const stamp = ExportOriginMemo::parse(*innerTx);
|
||||
if (stamp && stamp.value().origin.transactionHash == origin)
|
||||
{
|
||||
// Import consumes this latch after strong hooks finish.
|
||||
return Unexpected(PREREQUISITE_NOT_MET);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (currentTx.getTxnType() == ttEXPORT)
|
||||
{
|
||||
if (auto const innerTx = ExportLedgerOps::innerExportedTx(currentTx);
|
||||
innerTx && matchesTicket(*innerTx))
|
||||
if (currentTx.isFieldPresent(sfAccount) &&
|
||||
currentTx.getAccountID(sfAccount) == account &&
|
||||
currentTx.getTransactionID() == origin)
|
||||
{
|
||||
// Export creates this callback latch before post-apply hooks run.
|
||||
// Letting the callback cancel it would make the later Import fail.
|
||||
// Export creates this latch before post-apply hooks run.
|
||||
return Unexpected(PREREQUISITE_NOT_MET);
|
||||
}
|
||||
}
|
||||
|
||||
TER const ter = ExportLedgerOps::cancelExportLatch(
|
||||
TER const ter = ExportLedgerOps::controlExportLatch(
|
||||
hookCtx.applyCtx.view(),
|
||||
hookCtx.applyCtx.rawView(),
|
||||
account,
|
||||
ticketSeq,
|
||||
origin,
|
||||
flags == tfExportEraseLatch,
|
||||
j);
|
||||
|
||||
if (!isTesSuccess(ter))
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
return ticketSeq;
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
|
||||
@@ -2912,11 +2912,22 @@ DEFINE_HOOK_FUNCTION(int64_t, xport_reserve, uint32_t count)
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
DEFINE_HOOK_FUNCTION(int64_t, xport_cancel, uint32_t ticket_seq)
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
xport_cancel,
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len,
|
||||
uint32_t flags)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
auto const result = api.xport_cancel(ticket_seq);
|
||||
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
if (read_len != uint256::bytes)
|
||||
return INVALID_ARGUMENT;
|
||||
|
||||
auto const result =
|
||||
api.xport_cancel(uint256::fromVoid(memory + read_ptr), flags);
|
||||
if (!result)
|
||||
return result.error();
|
||||
return result.value();
|
||||
|
||||
@@ -376,7 +376,7 @@ Change::applyExportSignatures()
|
||||
auto const account = target->getAccountID(sfAccount);
|
||||
auto const latchKey = keylet::exportLatch(account, origin);
|
||||
auto const latch = view().read(latchKey);
|
||||
// A concurrently ordered cancel/expiry may remove the latch after the
|
||||
// A concurrently ordered explicit erase may remove the latch after the
|
||||
// accepted sidecar selected this witness. The historical evidence remains
|
||||
// valid, but there is no state transition left to perform.
|
||||
if (!latch)
|
||||
|
||||
@@ -31,11 +31,18 @@ Export::preflight(PreflightContext const& ctx)
|
||||
if (!isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
// Exactly one operation: export OR cancel, not both.
|
||||
bool const hasExport = ctx.tx.isFieldPresent(sfExportedTxn);
|
||||
bool const hasCancel = ctx.tx.isFieldPresent(sfCancelTicketSequence);
|
||||
if (ctx.tx.getFlags() & tfExportMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
if (hasExport == hasCancel) // neither or both
|
||||
// Exactly one operation: create an Export OR control an existing W.
|
||||
bool const hasExport = ctx.tx.isFieldPresent(sfExportedTxn);
|
||||
bool const hasControl = ctx.tx.isFieldPresent(sfTransactionHash);
|
||||
|
||||
if (hasExport == hasControl) // neither or both
|
||||
return temMALFORMED;
|
||||
if (hasExport && (ctx.tx.getFlags() & tfExportEraseLatch) != 0)
|
||||
return temMALFORMED;
|
||||
if (hasControl && ctx.tx.getFieldH256(sfTransactionHash).isZero())
|
||||
return temMALFORMED;
|
||||
|
||||
if (hasExport)
|
||||
@@ -64,8 +71,7 @@ Export::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
|
||||
// Exported transactions can retry across consensus rounds; every retrying
|
||||
// export needs an explicit outer expiry. Cancel-only exports are
|
||||
// immediate.
|
||||
// export needs an explicit outer expiry. Lifecycle controls are immediate.
|
||||
if (hasExport && !ctx.tx.isFieldPresent(sfLastLedgerSequence))
|
||||
return temMALFORMED;
|
||||
|
||||
@@ -127,12 +133,13 @@ Export::doApply()
|
||||
{
|
||||
auto const account = ctx_.tx.getAccountID(sfAccount);
|
||||
|
||||
// --- Export latch cancel path (mutually exclusive with export) ---
|
||||
if (ctx_.tx.isFieldPresent(sfCancelTicketSequence))
|
||||
// --- Export latch control path (mutually exclusive with export) ---
|
||||
if (ctx_.tx.isFieldPresent(sfTransactionHash))
|
||||
{
|
||||
auto const ticketSeq = ctx_.tx.getFieldU32(sfCancelTicketSequence);
|
||||
return ExportLedgerOps::cancelExportLatch(
|
||||
view(), ctx_.rawView(), account, ticketSeq, j_);
|
||||
auto const origin = ctx_.tx.getFieldH256(sfTransactionHash);
|
||||
bool const erase = (ctx_.tx.getFlags() & tfExportEraseLatch) != 0;
|
||||
return ExportLedgerOps::controlExportLatch(
|
||||
view(), ctx_.rawView(), account, origin, erase, j_);
|
||||
}
|
||||
|
||||
// --- Export intent path ---
|
||||
|
||||
@@ -390,7 +390,86 @@ eraseExportLatch(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Deterministically reclaim expired pending latches during paid Export work.
|
||||
/// Stop publication while retaining callback readiness and owner reserve.
|
||||
inline TER
|
||||
stopExportPublication(
|
||||
ApplyView& view,
|
||||
RawView& rawView,
|
||||
Keylet const& latchKey,
|
||||
beast::Journal j)
|
||||
{
|
||||
Sandbox sb{&view};
|
||||
auto latch = sb.peek(latchKey);
|
||||
if (!latch || latch->getType() != ltEXPORT_LATCH ||
|
||||
!latch->isFieldPresent(sfAccount) ||
|
||||
!latch->isFieldPresent(sfTransactionHash))
|
||||
return tecNO_ENTRY;
|
||||
|
||||
auto const account = latch->getAccountID(sfAccount);
|
||||
auto const origin = latch->getFieldH256(sfTransactionHash);
|
||||
if (keylet::exportLatch(account, origin).key != latchKey.key)
|
||||
return tefBAD_LEDGER;
|
||||
|
||||
if (auto const ter = removePendingExportLink(sb, latchKey, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
latch = sb.peek(latchKey);
|
||||
if (!latch)
|
||||
return tefBAD_LEDGER;
|
||||
auto const flags = latch->isFieldPresent(sfFlags)
|
||||
? latch->getFieldU32(sfFlags)
|
||||
: std::uint32_t{0};
|
||||
latch->setFieldU32(sfFlags, flags | lsfExportCanceled);
|
||||
sb.update(latch);
|
||||
sb.apply(rawView);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** Apply an owner-authorized lifecycle operation to one exact Export W.
|
||||
|
||||
Retain mode stops publication but preserves callback readiness. Erase mode
|
||||
removes the latch and knowingly forfeits any later callback.
|
||||
*/
|
||||
inline TER
|
||||
controlExportLatch(
|
||||
ApplyView& view,
|
||||
RawView& rawView,
|
||||
AccountID const& account,
|
||||
uint256 const& origin,
|
||||
bool erase,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto const key = keylet::exportLatch(account, origin);
|
||||
auto const latch = view.read(key);
|
||||
if (!latch)
|
||||
{
|
||||
JLOG(j.warn()) << "ExportLedgerOps: no Export latch for " << account
|
||||
<< " origin=" << origin;
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
if (latch->getType() != ltEXPORT_LATCH ||
|
||||
!latch->isFieldPresent(sfAccount) ||
|
||||
!latch->isFieldPresent(sfTransactionHash) ||
|
||||
latch->getAccountID(sfAccount) != account ||
|
||||
latch->getFieldH256(sfTransactionHash) != origin)
|
||||
return tefBAD_LEDGER;
|
||||
|
||||
auto const ter = erase ? eraseExportLatch(view, rawView, key, j)
|
||||
: stopExportPublication(view, rawView, key, j);
|
||||
if (isTesSuccess(ter))
|
||||
{
|
||||
JLOG(j.debug()) << "ExportLedgerOps: " << (erase ? "erased" : "stopped")
|
||||
<< " Export latch for " << account
|
||||
<< " origin=" << origin;
|
||||
}
|
||||
return ter;
|
||||
}
|
||||
|
||||
/** Deterministically stop expired publication work during paid Export work.
|
||||
|
||||
Expiry releases the global signing-work slot, but the latch and owner
|
||||
reserve remain until symmetric completion or an explicit erase election.
|
||||
*/
|
||||
inline TER
|
||||
pruneExpiredExportLatches(
|
||||
@@ -416,7 +495,7 @@ pruneExpiredExportLatches(
|
||||
|
||||
for (auto const& key : expired)
|
||||
{
|
||||
auto const ter = eraseExportLatch(view, rawView, key, j);
|
||||
auto const ter = stopExportPublication(view, rawView, key, j);
|
||||
if (!isTesSuccess(ter) && ter != tecNO_ENTRY)
|
||||
return ter;
|
||||
}
|
||||
@@ -646,80 +725,6 @@ validateTicketSequence(STTx const& stx, beast::Journal j)
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/// Cancel publication work for an account-owned Export latch.
|
||||
/// The latch retains its owner link and reserve so an already-executed target
|
||||
/// transaction may still complete its callback.
|
||||
///
|
||||
/// @param view The apply view to modify
|
||||
/// @param account The owning account
|
||||
/// @param ticketSeq The ticket sequence to cancel
|
||||
/// @param j Journal for logging
|
||||
/// @return tesSUCCESS or tecNO_ENTRY
|
||||
inline TER
|
||||
cancelExportLatch(
|
||||
ApplyView& view,
|
||||
RawView& rawView,
|
||||
AccountID const& account,
|
||||
std::uint32_t ticketSeq,
|
||||
beast::Journal j)
|
||||
{
|
||||
//@@start current-export-latch-cancel
|
||||
std::optional<Keylet> found;
|
||||
bool ambiguous = false;
|
||||
forEachItem(view, account, [&](std::shared_ptr<SLE const> const& item) {
|
||||
if (!item || item->getType() != ltEXPORT_LATCH ||
|
||||
!item->isFieldPresent(sfTransactionHash) ||
|
||||
item->getFieldU32(sfTicketSequence) != ticketSeq)
|
||||
return;
|
||||
if (found)
|
||||
ambiguous = true;
|
||||
found =
|
||||
keylet::exportLatch(account, item->getFieldH256(sfTransactionHash));
|
||||
});
|
||||
if (ambiguous)
|
||||
return tefBAD_LEDGER;
|
||||
|
||||
if (!found)
|
||||
{
|
||||
JLOG(j.warn()) << "ExportLedgerOps: no Export latch to cancel for "
|
||||
<< account << " seq=" << ticketSeq;
|
||||
return tecNO_ENTRY;
|
||||
}
|
||||
|
||||
auto const key = *found;
|
||||
auto const sle = view.read(key);
|
||||
if (!sle || !sle->isFieldPresent(sfAccount) ||
|
||||
sle->getAccountID(sfAccount) != account)
|
||||
{
|
||||
JLOG(j.warn()) << "ExportLedgerOps: Export latch ownership mismatch";
|
||||
return tecNO_PERMISSION;
|
||||
}
|
||||
if (keylet::exportLatch(account, sle->getFieldH256(sfTransactionHash))
|
||||
.key != key.key)
|
||||
return tefBAD_LEDGER;
|
||||
|
||||
Sandbox sb{&view};
|
||||
if (auto const ter = removePendingExportLink(sb, key, j);
|
||||
!isTesSuccess(ter))
|
||||
return ter;
|
||||
|
||||
auto latch = sb.peek(key);
|
||||
if (!latch)
|
||||
return tefBAD_LEDGER;
|
||||
auto const flags = latch->isFieldPresent(sfFlags)
|
||||
? latch->getFieldU32(sfFlags)
|
||||
: std::uint32_t{0};
|
||||
latch->setFieldU32(sfFlags, flags | lsfExportCanceled);
|
||||
sb.update(latch);
|
||||
sb.apply(rawView);
|
||||
|
||||
JLOG(j.debug()) << "ExportLedgerOps: canceled Export latch for " << account
|
||||
<< " seq=" << ticketSeq;
|
||||
|
||||
//@@end current-export-latch-cancel
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
} // namespace ExportLedgerOps
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -1429,8 +1429,8 @@ Import::doApply()
|
||||
return ter;
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Export callback path: ticket-based import consumes the shadow
|
||||
// ticket and fires hooks — no B2M crediting, no account creation.
|
||||
// Export callback path: ticket-based target execution records the Export
|
||||
// latch fact and fires hooks — no B2M crediting, no account creation.
|
||||
// The hook inspects the result via xpop_slot().
|
||||
// ---------------------------------------------------------------
|
||||
if (path == ImportPath::exportCallback)
|
||||
|
||||
Reference in New Issue
Block a user