refactor(export): store multisigned tx as sfExportedTxn object in metadata

Use sfExportedTxn (OBJECT) instead of sfBlob (VL) for the multisigned
transaction in sfExportResult metadata. This renders as readable JSON
with all fields visible (Account, Signers, etc.) instead of an opaque
hex blob.

Also compute the tx hash directly via getHash(HashPrefix::transactionID)
on the STObject instead of serializing/deserializing through STTx.
This commit is contained in:
Nicholas Dudfield
2026-03-18 14:16:25 +07:00
parent 8c747a1916
commit de43ca2385
2 changed files with 37 additions and 21 deletions

View File

@@ -695,8 +695,20 @@ struct Export_test : public beast::unit_test::suite
{
auto const& result =
exportMeta->peekAtField(sfExportResult).downcast<STObject>();
if (result.isFieldPresent(sfBlob))
multisignedBlob = result.getFieldVL(sfBlob);
if (result.isFieldPresent(sfExportedTxn))
{
// Serialize the nested object to get the raw blob
// for submission to XRPL.
auto const& expTxn = const_cast<STObject&>(result)
.peekFieldObject(sfExportedTxn);
Serializer s;
expTxn.add(s);
multisignedBlob = s.peekData();
log << "Xahau: ExportResult.ExportedTxn = "
<< expTxn.getJson(JsonOptions::none).toStyledString()
<< std::endl;
}
}
log << "Xahau: multisigned blob size = " << multisignedBlob.size()
<< std::endl;

View File

@@ -8,6 +8,7 @@
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
@@ -228,29 +229,31 @@ Export::doApply()
return a.getAccountID(sfAccount) < b.getAccountID(sfAccount);
});
// Build the multisigned STTx: inner tx + empty SigningPubKey
// + Signers array.
STObject multiSigned(innerTx);
if (multiSigned.isFieldPresent(sfTxnSignature))
multiSigned.makeFieldAbsent(sfTxnSignature);
// Build the multisigned tx. Use sfExportedTxn as the field type
// so it nests properly in ExportResult metadata as readable JSON.
STObject multiSigned(sfExportedTxn);
{
// Copy all non-signing fields from innerTx, then we'll add
// signing fields (empty SigningPubKey + Signers) below.
Serializer s;
innerTx.addWithoutSigningFields(s);
SerialIter sit(s.slice());
multiSigned.set(sit);
}
// Set empty SigningPubKey (indicates multisigned).
multiSigned.setFieldVL(sfSigningPubKey, Slice{});
if (signers.size() > 0)
multiSigned.setFieldArray(sfSigners, signers);
// Serialize to get the blob, then deserialize as STTx to
// compute the correct transaction ID (includes Signers).
Serializer outSer;
multiSigned.add(outSer);
Blob multisignedBlob = outSer.peekData();
// Compute the signed tx hash for the shadow ticket.
// getHash(transactionID) includes ALL fields (Signers etc.),
// matching what STTx::getTransactionID() produces.
auto const signedTxHash =
multiSigned.getHash(HashPrefix::transactionID);
SerialIter finalSit(outSer.slice());
STTx const signedTx(std::ref(finalSit));
auto const signedTxHash = signedTx.getTransactionID();
// Now create the shadow ticket with the signed tx hash.
// Create the shadow ticket with the signed tx hash.
{
TER ter = ExportLedgerOps::createShadowTicket(
view(), account, innerTx, signedTxHash, j_);
@@ -258,12 +261,13 @@ Export::doApply()
return ter;
}
// Write the export result to metadata.
// Write the export result to metadata. The multisigned tx is
// stored as sfExportedTxn (OBJECT) so it renders as readable
// JSON in metadata, not an opaque hex blob.
STObject exportResult(sfExportResult);
exportResult.setFieldU32(sfLedgerSequence, currentSeq);
exportResult.setFieldH256(sfTransactionHash, txId);
if (!multisignedBlob.empty())
exportResult.setFieldVL(sfBlob, multisignedBlob);
exportResult.set(std::move(multiSigned));
auto* avi = dynamic_cast<ApplyViewImpl*>(&view());
if (!avi)