From de43ca2385a30d902bdfa3775a8b8fb1ea91f05b Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Wed, 18 Mar 2026 14:16:25 +0700 Subject: [PATCH] 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. --- src/test/app/Export_test.cpp | 16 ++++++++++-- src/xrpld/app/tx/detail/Export.cpp | 42 ++++++++++++++++-------------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/test/app/Export_test.cpp b/src/test/app/Export_test.cpp index 36b1451a8..725d7d1f9 100644 --- a/src/test/app/Export_test.cpp +++ b/src/test/app/Export_test.cpp @@ -695,8 +695,20 @@ struct Export_test : public beast::unit_test::suite { auto const& result = exportMeta->peekAtField(sfExportResult).downcast(); - 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(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; diff --git a/src/xrpld/app/tx/detail/Export.cpp b/src/xrpld/app/tx/detail/Export.cpp index 9d179d807..a98780264 100644 --- a/src/xrpld/app/tx/detail/Export.cpp +++ b/src/xrpld/app/tx/detail/Export.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -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(&view()); if (!avi)