fix(export): compute emit fee before STTx construction

Mutating the fee via const_cast after STTx construction left a stale
cached getTransactionID(). When the emitted ttEXPORT was serialised
into the emitted directory and later deserialised, the round-tripped
txid differed from the original, causing tefNONDIR_EMIT in
Transactor::preclaim (the emitted dir entry was keyed with the stale
hash).

Build a throwaway STTx with fee=0 to calculate the fee size, then
construct the real STTx with the correct fee from the start.
This commit is contained in:
Nicholas Dudfield
2026-03-18 17:04:46 +07:00
parent 02a0552325
commit a8097cd9a6

View File

@@ -1304,14 +1304,61 @@ HookAPI::xport(Slice const& txBlob) const
Serializer innerSer;
innerTx->add(innerSer);
// Build the ttEXPORT wrapper with EmitDetails.
// Pre-compute the emit fee so we can set it during STTx
// construction. Mutating the fee after construction via
// const_cast leaves a stale cached getTransactionID(), which
// breaks the tefNONDIR_EMIT check in Transactor::preclaim
// after the emitted tx is serialised and deserialised through
// the emitted directory round-trip.
//
// We build a throwaway STTx with fee=0 just for the size
// calculation, then construct the real one with the correct fee.
uint64_t emitFee = 0;
{
STTx tmp(ttEXPORT, [&](auto& obj) {
obj[sfAccount] = hookCtx.result.account;
obj[sfSequence] = 0u;
obj.setFieldVL(sfSigningPubKey, Blob{});
obj[sfFirstLedgerSequence] = ledgerSeq + 1;
obj[sfLastLedgerSequence] = ledgerSeq + 5;
obj[sfFee] = STAmount{0};
SerialIter sit(innerSer.slice());
obj.set(std::make_unique<STObject>(sit, sfExportedTxn));
STObject ed(sfEmitDetails);
ed.setFieldU32(
sfEmitGeneration, static_cast<uint32_t>(etxn_generation()));
{
auto const b = etxn_burden();
ed.setFieldU64(sfEmitBurden, b ? uint64_t(*b) : 1ULL);
}
ed.setFieldH256(
sfEmitParentTxnID, applyCtx.tx.getTransactionID());
ed.setFieldH256(sfEmitNonce, *nonce);
ed.setFieldH256(sfEmitHookHash, hookCtx.result.hookHash);
if (hookCtx.result.hasCallback)
ed.setAccountID(sfEmitCallback, hookCtx.result.account);
obj.set(std::move(ed));
});
Serializer feeSer;
tmp.add(feeSer);
auto feeResult = etxn_fee_base(feeSer.slice());
if (!feeResult)
{
JLOG(j.trace()) << "HookExport[" << HC_ACC()
<< "]: Fee calculation failed for ttEXPORT wrapper";
return Unexpected(EXPORT_FAILURE);
}
emitFee = static_cast<uint64_t>(*feeResult);
}
// Build the ttEXPORT wrapper with the correct fee.
STTx exportStx(ttEXPORT, [&](auto& obj) {
obj[sfAccount] = hookCtx.result.account;
obj[sfSequence] = 0u;
obj.setFieldVL(sfSigningPubKey, Blob{});
obj[sfFirstLedgerSequence] = ledgerSeq + 1;
obj[sfLastLedgerSequence] = ledgerSeq + 5;
obj[sfFee] = STAmount{0}; // emitted txns have special fee handling
obj[sfFee] = STAmount{emitFee};
// sfExportedTxn inner object
SerialIter sit(innerSer.slice());
@@ -1336,21 +1383,6 @@ HookAPI::xport(Slice const& txBlob) const
obj.set(std::move(emitDetails));
});
// Calculate proper fee for the emitted transaction.
{
Serializer feeSer;
exportStx.add(feeSer);
auto feeResult = etxn_fee_base(feeSer.slice());
if (!feeResult)
{
JLOG(j.trace()) << "HookExport[" << HC_ACC()
<< "]: Fee calculation failed for ttEXPORT wrapper";
return Unexpected(EXPORT_FAILURE);
}
const_cast<STTx&>(exportStx).setFieldAmount(
sfFee, STAmount{static_cast<uint64_t>(*feeResult)});
}
// Preflight the wrapper.
auto preflightResult = ripple::preflight(
app, view.rules(), exportStx, ripple::ApplyFlags::tapPREFLIGHT_EMIT, j);