fix(export): verify multisign signatures at ingestion time

C3: Cryptographically verify each export signature blob against the
inner transaction's signing data before storing in the collector.
Looks up the ttEXPORT tx from the open ledger, reconstructs the
signing data via buildMultiSigningData, and calls verify().

If the tx isn't in our open ledger yet (timing/relay), the sig is
stored unverified as a fallback — it can be verified later at the
SHAMap merge path or will be rejected at Export::doApply if invalid.

This runs on the jtPROPOSAL_t job queue thread (not the IO strand
or transactor), so the verify() cost has no impact on consensus
critical path performance.
This commit is contained in:
Nicholas Dudfield
2026-04-09 14:43:30 +07:00
parent 57c46c61fc
commit b80352e512

View File

@@ -1518,7 +1518,27 @@ ConsensusExtensions::onTrustedPeerMessage(
}
}
// Pass 2: commit validated sigs.
// Pass 2: verify multisign signatures and commit.
// Look up each export tx from the open ledger to reconstruct the
// signing data. This runs on a jtPROPOSAL_t job queue thread
// (post-C1 fix), so the verify() cost doesn't block the IO strand
// or the single-threaded transactor.
auto const openLedger = app_.openLedger().current();
// Build a txHash -> STTx lookup for ttEXPORT txns in the open ledger.
// Avoids O(N*M) iteration when processing multiple export sig blobs.
std::unordered_map<uint256, std::shared_ptr<STTx const>> exportTxns;
if (openLedger)
{
for (auto const& [stx, meta] : openLedger->txs)
{
if (stx && stx->getTxnType() == ttEXPORT)
exportTxns.emplace(stx->getTransactionID(), stx);
}
}
auto const signerAcctID = calcAccountID(senderPK);
for (int i = 0; i < wireMsg.exportsignatures_size(); ++i)
{
auto const& blob = wireMsg.exportsignatures(i);
@@ -1529,17 +1549,55 @@ ConsensusExtensions::onTrustedPeerMessage(
uint256 txHash;
std::memcpy(txHash.data(), blob.data(), 32);
if (blob.size() > 65)
{
auto const fullSlice = makeSlice(blob);
auto const sigSlice = fullSlice.substr(65);
Buffer sigBuf(sigSlice.data(), sigSlice.size());
exportSigCollector_.addSignature(txHash, senderPK, sigBuf);
}
else
if (blob.size() <= 65)
{
// Pubkey-only entry (no real signature).
exportSigCollector_.addSignature(txHash, senderPK);
continue;
}
auto const fullSlice = makeSlice(blob);
auto const sigSlice = fullSlice.substr(65);
Buffer sigBuf(sigSlice.data(), sigSlice.size());
// Verify the multisign signature against the inner tx if
// we can find it in the open ledger. If the tx isn't in
// our open ledger yet (timing / relay order), store the sig
// unverified — it will be verified at the SHAMap merge path
// or rejected at Export::doApply if invalid.
auto const txIt = exportTxns.find(txHash);
if (txIt != exportTxns.end() &&
txIt->second->isFieldPresent(sfExportedTxn))
{
try
{
auto const& exportedObj = const_cast<STTx&>(*txIt->second)
.peekAtField(sfExportedTxn)
.downcast<STObject>();
Serializer innerSer;
exportedObj.add(innerSer);
SerialIter sit(innerSer.slice());
STTx innerTx(std::ref(sit));
auto const sigData =
buildMultiSigningData(innerTx, signerAcctID);
if (!verify(senderPK, sigData.slice(), sigSlice))
{
JLOG(j_.warn()) << "Export: invalid multisign sig for tx "
<< txHash << " — rejected";
continue;
}
}
catch (std::exception const& e)
{
JLOG(j_.warn()) << "Export: failed to verify sig for tx "
<< txHash << ": " << e.what();
continue;
}
}
exportSigCollector_.addSignature(txHash, senderPK, sigBuf);
}
}
//@@end peer-harvest-export-sigs