fix(export): preserve witness signer attribution

This commit is contained in:
Nicholas Dudfield
2026-07-14 19:02:01 +07:00
parent 9286edc57f
commit da9e6b3cc7
13 changed files with 273 additions and 104 deletions

View File

@@ -265,21 +265,14 @@ def assert_hook_accepted(meta, log, *, expected_emits=1, expected_exports=None):
def _signer_entries(witness):
entries = []
exported = witness.get("ExportedTxn", {})
for entry in exported.get("Signers", []):
signer = entry.get("Signer", entry)
for entry in witness.get("ExportSigners", []):
signer = entry.get("ExportSigner", entry)
entries.append(signer)
return entries
def _account_sort_key(address):
from xrpl.core.addresscodec import decode_classic_address
return decode_classic_address(address)
def assert_export_witness(witness, origin_hash, ledger_seq, log):
"""Assert a later-ledger witness contains one canonical target assembly."""
"""Assert a later-ledger witness contains one ordered signature record."""
if witness.get("TransactionType") != "ExportSignatures":
raise AssertionError("Expected ExportSignatures witness")
if witness.get("TransactionHash") != origin_hash:
@@ -287,16 +280,26 @@ def assert_export_witness(witness, origin_hash, ledger_seq, log):
if witness.get("LedgerSequence") != ledger_seq:
raise AssertionError("ExportSignatures ledger binding mismatch")
if witness.get("Signers"):
raise AssertionError("Witness Signers must be nested under ExportedTxn")
if not witness.get("EntropyContributors"):
raise AssertionError("Witness must not contain ordinary Signers")
if witness.get("ExportedTxn", {}).get("Signers"):
raise AssertionError("Witness ExportedTxn must be unsigned")
contributors = witness.get("EntropyContributors")
if not contributors:
raise AssertionError("ExportSignatures missing contributor bitmap")
signers = _signer_entries(witness)
if not signers:
raise AssertionError("ExportSignatures assembled transaction has no Signers")
accounts = [signer.get("Account") for signer in signers]
if accounts != sorted(accounts, key=_account_sort_key):
raise AssertionError("ExportSignatures Signers are not Account-sorted")
raise AssertionError("ExportSignatures has no ExportSigners")
if any(
not signer.get("SigningPubKey") or not signer.get("TxnSignature")
for signer in signers
):
raise AssertionError("ExportSignatures has a malformed ExportSigner")
contributor_count = sum(byte.bit_count() for byte in bytes.fromhex(contributors))
if contributor_count != len(signers):
raise AssertionError(
"ExportSignatures contributor bitmap and ordered signer count differ"
)
log(f" Witness signers: {len(signers)} validator(s)")
witness["_WitnessSigners"] = signers
return witness

View File

@@ -377,7 +377,7 @@ UNTYPED_SFIELD(sfHook, OBJECT, 14)
// inner object (uncommon)
UNTYPED_SFIELD(sfSigner, OBJECT, 16)
// 17 unused
UNTYPED_SFIELD(sfExportSigner, OBJECT, 17)
UNTYPED_SFIELD(sfMajority, OBJECT, 18)
UNTYPED_SFIELD(sfDisabledValidator, OBJECT, 19)
UNTYPED_SFIELD(sfEmittedTxn, OBJECT, 20)
@@ -429,7 +429,7 @@ UNTYPED_SFIELD(sfHookParameters, ARRAY, 19)
UNTYPED_SFIELD(sfHookGrants, ARRAY, 20)
UNTYPED_SFIELD(sfXChainClaimAttestations, ARRAY, 21)
UNTYPED_SFIELD(sfXChainCreateAccountAttestations, ARRAY, 22)
// 23 unused
UNTYPED_SFIELD(sfExportSigners, ARRAY, 23)
UNTYPED_SFIELD(sfPriceDataSeries, ARRAY, 24)
UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25)
UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)

View File

@@ -634,4 +634,5 @@ TRANSACTION(ttEXPORT_SIGNATURES, 106, ExportSignatures, ({
{sfTransactionHash, soeREQUIRED},
{sfExportedTxn, soeREQUIRED},
{sfEntropyContributors, soeREQUIRED},
{sfExportSigners, soeREQUIRED},
}))

View File

@@ -53,6 +53,13 @@ InnerObjectFormats::InnerObjectFormats()
{sfTxnSignature, soeREQUIRED},
});
add(sfExportSigner.jsonName,
sfExportSigner.getCode(),
{
{sfSigningPubKey, soeREQUIRED},
{sfTxnSignature, soeREQUIRED},
});
add(sfMajority.jsonName,
sfMajority.getCode(),
{

View File

@@ -18,6 +18,7 @@
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
@@ -88,6 +89,20 @@ makeExportedPaymentChannelClaim(
return makeSTTx(obj);
}
ExportResultBuilder::PositionedSignatureSnapshot
positionedSignatures(ExportResultBuilder::SignatureSnapshot const& signatures)
{
ExportResultBuilder::PositionedSignatureSnapshot result;
std::uint16_t position = 0;
for (auto const& [key, signature] : signatures)
{
result.emplace(
position++,
ExportResultBuilder::PositionedSignature{key, signature});
}
return result;
}
} // namespace
class ExportResultBuilder_test : public beast::unit_test::suite
@@ -313,7 +328,7 @@ public:
calcAccountID(src.first), calcAccountID(dst.first));
ExportResultBuilder::SignatureSnapshot signatures;
while (signatures.size() < STTx::maxMultiSigners() + 5)
while (signatures.size() < STTx::maxMultiSigners())
{
auto const signer = randomKeyPair(KeyType::secp256k1);
signatures.emplace(
@@ -391,7 +406,7 @@ public:
auto const exportTxHash = makeHash("outer-export-witness");
ExportResultBuilder::SignatureSnapshot signatures;
while (signatures.size() < STTx::maxMultiSigners() + 5)
while (signatures.size() < STTx::maxMultiSigners())
{
auto const signer = randomKeyPair(KeyType::secp256k1);
signatures.emplace(
@@ -400,10 +415,11 @@ public:
innerTx, signer.first, signer.second));
}
auto const positioned = positionedSignatures(signatures);
Blob const contributors(
(STTx::maxMultiSigners() + 7) / 8, std::uint8_t{0xFF});
STTx::maxMultiSigners() / 8, std::uint8_t{0xFF});
auto witness = ExportResultBuilder::buildSignatureWitness(
exportTxHash, innerTx, signatures, contributors, 654);
exportTxHash, innerTx, positioned, STTx::maxMultiSigners(), 654);
BEAST_EXPECT(witness.getTxnType() == ttEXPORT_SIGNATURES);
BEAST_EXPECT(witness.getFieldU32(sfLedgerSequence) == 654);
BEAST_EXPECT(witness.getFieldH256(sfTransactionHash) == exportTxHash);
@@ -413,8 +429,9 @@ public:
auto const& exported =
witness.peekAtField(sfExportedTxn).downcast<STObject>();
BEAST_EXPECT(exported.getFieldU16(sfTransactionType) == ttPAYMENT);
BEAST_EXPECT(!exported.isFieldPresent(sfSigners));
BEAST_EXPECT(
exported.getFieldArray(sfSigners).size() ==
witness.getFieldArray(sfExportSigners).size() ==
STTx::maxMultiSigners());
auto decoded = ExportResultBuilder::signaturesFromWitness(witness);
@@ -422,12 +439,14 @@ public:
if (decoded)
{
BEAST_EXPECT(decoded->size() == STTx::maxMultiSigners());
for (auto const& [pk, sig] : *decoded)
std::uint16_t expectedPosition = 0;
for (auto const& [position, signature] : *decoded)
{
auto const it = signatures.find(pk);
BEAST_EXPECT(position == expectedPosition++);
auto const it = signatures.find(signature.signingKey);
BEAST_EXPECT(it != signatures.end());
if (it != signatures.end())
BEAST_EXPECT(it->second == sig);
BEAST_EXPECT(it->second == signature.signature);
}
}
}
@@ -435,7 +454,7 @@ public:
void
testRejectsNonCanonicalWitnessSigner()
{
testcase("signature witness validates signer account");
testcase("signature witness rejects non-canonical signer entry");
auto const src = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
@@ -445,20 +464,49 @@ public:
calcAccountID(src.first), calcAccountID(dst.first));
auto const exportTxHash = makeHash("bad-witness-signer");
ExportResultBuilder::SignatureSnapshot signatures;
ExportResultBuilder::PositionedSignatureSnapshot signatures;
signatures.emplace(
signer.first,
ExportResultBuilder::signExportedTxn(
innerTx, signer.first, signer.second));
0,
ExportResultBuilder::PositionedSignature{
signer.first,
ExportResultBuilder::signExportedTxn(
innerTx, signer.first, signer.second)});
auto witness = ExportResultBuilder::buildSignatureWitness(
exportTxHash, innerTx, signatures, Blob{0x01}, 654);
auto& exported = witness.peekFieldObject(sfExportedTxn);
auto signers = exported.getFieldArray(sfSigners);
exportTxHash, innerTx, signatures, 1, 654);
auto signers = witness.getFieldArray(sfExportSigners);
signers[0].setAccountID(sfAccount, calcAccountID(wrongAccount.first));
exported.setFieldArray(sfSigners, signers);
witness.setFieldArray(sfExportSigners, signers);
BEAST_EXPECT(!ExportResultBuilder::signaturesFromWitness(witness));
except([&] {
Serializer serialized;
witness.add(serialized);
SerialIter iter{serialized.slice()};
STTx decoded{iter};
(void)decoded;
});
auto malformedBitmap = ExportResultBuilder::buildSignatureWitness(
exportTxHash, innerTx, signatures, 1, 654);
malformedBitmap.setFieldVL(sfEntropyContributors, Blob{0x00});
BEAST_EXPECT(
!ExportResultBuilder::signaturesFromWitness(malformedBitmap));
ExportResultBuilder::PositionedSignatureSnapshot oversized;
for (std::uint16_t position = 0;
position <= ExportLimits::maxCommitteeMembers;
++position)
{
auto const key = randomKeyPair(KeyType::secp256k1).first;
oversized.emplace(
position,
ExportResultBuilder::PositionedSignature{key, Buffer{0x01}});
}
except([&] {
ExportResultBuilder::buildSignatureWitness(
exportTxHash, innerTx, oversized, oversized.size(), 654);
});
}
void
@@ -509,13 +557,12 @@ public:
auto const multiSigned =
ExportResultBuilder::buildMultiSignedExportedTxn(
innerTx, signatures);
Blob const contributors(
(STTx::maxMultiSigners() + 7) / 8, std::uint8_t{0xFF});
auto const positioned = positionedSignatures(signatures);
auto const witness = ExportResultBuilder::buildSignatureWitness(
makeHash("size-inventory-export"),
innerTx,
signatures,
contributors,
positioned,
STTx::maxMultiSigners(),
654);
auto const innerBytes = innerTx.getSerializer().size();
@@ -536,13 +583,16 @@ public:
BEAST_EXPECT(!witness.isFieldPresent(sfSigners));
auto const& exported =
witness.peekAtField(sfExportedTxn).downcast<STObject>();
BEAST_EXPECT(!exported.isFieldPresent(sfSigners));
BEAST_EXPECT(
exported.getFieldArray(sfSigners).size() ==
witness.getFieldArray(sfExportSigners).size() ==
STTx::maxMultiSigners());
Blob const contributors(
STTx::maxMultiSigners() / 8, std::uint8_t{0xFF});
BEAST_EXPECT(witness.getFieldVL(sfEntropyContributors) == contributors);
BEAST_EXPECT(innerBytes == 163);
BEAST_EXPECT(multiSignedBytes == 4453);
BEAST_EXPECT(selfContainedWitnessBytes > multiSignedBytes);
BEAST_EXPECT(selfContainedWitnessBytes == 3839);
BEAST_EXPECT(legacyShareBytes * STTx::maxMultiSigners() == 4384);
//@@end export-serialized-size-inventory
}

View File

@@ -157,8 +157,19 @@ struct Export_test : public beast::unit_test::suite
BEAST_EXPECT(!witnessTx->isFieldPresent(sfSigners));
BEAST_EXPECT(
witnessTx->getFieldVL(sfEntropyContributors) == Blob{0x01});
auto const& assembled =
auto const& base =
witnessTx->peekAtField(sfExportedTxn).downcast<STObject>();
auto const positioned =
ExportResultBuilder::signaturesFromWitness(*witnessTx);
BEAST_EXPECT(positioned);
if (!positioned)
continue;
ExportResultBuilder::SignatureSnapshot signatures;
for (auto const& [_, signature] : *positioned)
signatures.emplace(signature.signingKey, signature.signature);
auto const assembled =
ExportResultBuilder::buildMultiSignedExportedTxn(
makeSTTx(base), signatures);
Serializer s;
assembled.add(s);
multisignedBlob = s.peekData();
@@ -837,8 +848,8 @@ struct Export_test : public beast::unit_test::suite
BEAST_EXPECT(witness->getFieldVL(sfEntropyContributors) == Blob{0x01});
auto const& assembled =
witness->peekAtField(sfExportedTxn).downcast<STObject>();
BEAST_EXPECT(assembled.isFieldPresent(sfSigners));
BEAST_EXPECT(assembled.getFieldArray(sfSigners).size() == 1);
BEAST_EXPECT(!assembled.isFieldPresent(sfSigners));
BEAST_EXPECT(witness->getFieldArray(sfExportSigners).size() == 1);
auto const releasedLatch = env.closed()->read(latchKey);
BEAST_EXPECT(releasedLatch);
@@ -1322,25 +1333,27 @@ struct Export_test : public beast::unit_test::suite
if (!release)
return;
ExportResultBuilder::SignatureSnapshot signatures;
ExportResultBuilder::PositionedSignatureSnapshot signatures;
signatures.emplace(
valKeys.keys->publicKey,
ExportResultBuilder::signExportedTxn(
release.value(),
0,
ExportResultBuilder::PositionedSignature{
valKeys.keys->publicKey,
valKeys.keys->secretKey));
ExportResultBuilder::signExportedTxn(
release.value(),
valKeys.keys->publicKey,
valKeys.keys->secretKey)});
auto const witnessSeq = originLedger->seq() + 1;
auto const witnessTx = std::make_shared<STTx const>(
ExportResultBuilder::buildSignatureWitness(
origin, release.value(), signatures, Blob{0x01}, witnessSeq));
origin, release.value(), signatures, 1, witnessSeq));
BEAST_EXPECT(!witnessTx->isFieldPresent(sfSigners));
BEAST_EXPECT(
witnessTx->getFieldVL(sfEntropyContributors) == Blob{0x01});
auto const& assembled =
witnessTx->peekAtField(sfExportedTxn).downcast<STObject>();
BEAST_EXPECT(assembled.isFieldPresent(sfSigners));
BEAST_EXPECT(assembled.getFieldArray(sfSigners).size() == 1);
BEAST_EXPECT(!assembled.isFieldPresent(sfSigners));
BEAST_EXPECT(witnessTx->getFieldArray(sfExportSigners).size() == 1);
CanonicalTXSet txns{originLedger->info().hash};
txns.insert(witnessTx);

View File

@@ -85,10 +85,12 @@ struct PseudoTx_test : public beast::unit_test::suite
auto const secret = generateSecretKey(KeyType::secp256k1, randomSeed());
auto const publicKey = derivePublicKey(KeyType::secp256k1, secret);
ExportResultBuilder::SignatureSnapshot signatures;
ExportResultBuilder::PositionedSignatureSnapshot signatures;
std::uint8_t const signatureBytes[] = {1, 2, 3};
signatures.emplace(
publicKey, Buffer{signatureBytes, sizeof(signatureBytes)});
0,
ExportResultBuilder::PositionedSignature{
publicKey, Buffer{signatureBytes, sizeof(signatureBytes)}});
auto const releaseTarget = STTx(ttPAYMENT, [&](auto& obj) {
obj.setAccountID(sfAccount, AccountID(1));
obj.setAccountID(sfDestination, AccountID(2));
@@ -97,7 +99,7 @@ struct PseudoTx_test : public beast::unit_test::suite
obj.setFieldVL(sfSigningPubKey, Blob{});
});
res.emplace_back(ExportResultBuilder::buildSignatureWitness(
uint256(4), releaseTarget, signatures, Blob{0x01}, seq));
uint256(4), releaseTarget, signatures, 1, seq));
return res;
}

View File

@@ -222,14 +222,17 @@ makeExportSignaturesTx(std::uint32_t ledgerSeq, uint256 const& exportTxHash)
auto const innerObj = makeExportedPayment(calcAccountID(signer.first), dst);
auto const innerTx = makeSTTx(innerObj);
ExportResultBuilder::SignatureSnapshot signatures;
ExportResultBuilder::PositionedSignatureSnapshot signatures;
auto sig = ExportResultBuilder::signExportedTxn(
innerTx, signer.first, signer.second);
signatures.emplace(signer.first, Buffer(sig.data(), sig.size()));
signatures.emplace(
0,
ExportResultBuilder::PositionedSignature{
signer.first, Buffer(sig.data(), sig.size())});
return std::make_shared<STTx const>(
ExportResultBuilder::buildSignatureWitness(
exportTxHash, innerTx, signatures, Blob{0x01}, ledgerSeq));
exportTxHash, innerTx, signatures, 1, ledgerSeq));
}
RCLTxSet
@@ -2598,20 +2601,15 @@ class ConsensusExtensions_test : public beast::unit_test::suite
if (!material)
return;
BEAST_EXPECT(material->signatures.size() == 2);
BEAST_EXPECT(material->contributors == Blob{0x05});
auto const witness = ExportResultBuilder::buildSignatureWitness(
origin,
releaseTarget,
material->signatures,
material->contributors,
20);
origin, releaseTarget, material->signatures, 3, 20);
BEAST_EXPECT(!witness.isFieldPresent(sfSigners));
BEAST_EXPECT(witness.getFieldVL(sfEntropyContributors) == Blob{0x05});
auto const& assembled =
witness.peekAtField(sfExportedTxn).downcast<STObject>();
BEAST_EXPECT(assembled.isFieldPresent(sfSigners));
BEAST_EXPECT(assembled.getFieldArray(sfSigners).size() == 2);
BEAST_EXPECT(!assembled.isFieldPresent(sfSigners));
BEAST_EXPECT(witness.getFieldArray(sfExportSigners).size() == 2);
}
void

View File

@@ -2148,7 +2148,6 @@ ConsensusExtensions::agreedExportWitness(
return std::nullopt;
ExportWitnessMaterial material;
material.contributors.resize(validatorBitsetBytes(universeSize), 0);
std::set<std::uint32_t> positions;
hash_set<AccountID> signerAccounts;
bool invalid = false;
@@ -2221,14 +2220,14 @@ ConsensusExtensions::agreedExportWitness(
}
auto const [_, inserted] = material.signatures.emplace(
key, Buffer{signature.data(), signature.size()});
static_cast<std::uint16_t>(position),
ExportResultBuilder::PositionedSignature{
key, Buffer{signature.data(), signature.size()}});
if (!inserted)
{
invalid = true;
return;
}
material.contributors[position / 8] |=
static_cast<std::uint8_t>(1u << (position % 8));
}
catch (std::exception const& e)
{
@@ -2799,7 +2798,7 @@ ConsensusExtensions::onPreBuild(
origin,
release.value(),
material->signatures,
material->contributors,
validatorView->orderedOriginalMasterKeys.size(),
seq);
retriableTxs.insert(std::make_shared<STTx>(std::move(witness)));
}

View File

@@ -6,6 +6,7 @@
#include <xrpld/app/consensus/RCLCxPeerPos.h>
#include <xrpld/app/consensus/RCLCxTx.h>
#include <xrpld/app/misc/ExportSigCollectorV2.h>
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
#include <xrpld/consensus/ConsensusParms.h>
#include <xrpld/consensus/ConsensusTypes.h>
#include <xrpld/overlay/ExportShareAdmission.h>
@@ -126,8 +127,7 @@ public:
using ExportSignatureSnapshot = std::map<PublicKey, Buffer>;
struct ExportWitnessMaterial
{
ExportSignatureSnapshot signatures;
Blob contributors;
ExportResultBuilder::PositionedSignatureSnapshot signatures;
};
private:

View File

@@ -98,10 +98,8 @@ Change::preflight(PreflightContext const& ctx)
return temBAD_FEE;
}
bool const exportSignatureWitness =
ctx.tx.getTxnType() == ttEXPORT_SIGNATURES;
if (!ctx.tx.getSigningPubKey().empty() || !ctx.tx.getSignature().empty() ||
(ctx.tx.isFieldPresent(sfSigners) && !exportSignatureWitness))
ctx.tx.isFieldPresent(sfSigners))
{
JLOG(ctx.j.warn()) << "Change: Bad signature";
return temBAD_SIGNATURE;
@@ -427,14 +425,21 @@ Change::applyExportSignatures()
return tefFAILURE;
hash_set<AccountID> signerAccounts;
for (auto const& [key, signature] : *signatures)
for (auto const& [position, witness] : *signatures)
{
auto const signer = calcAccountID(key);
if (position >= committee.size() * 8 ||
(contributors[position / 8] &
static_cast<std::uint8_t>(1u << (position % 8))) == 0)
return tefFAILURE;
auto const signer = calcAccountID(witness.signingKey);
if (!signerAccounts.insert(signer).second)
return tefFAILURE;
auto const data = buildMultiSigningData(*target, signer);
if (!verify(
key, data.slice(), Slice{signature.data(), signature.size()}))
witness.signingKey,
data.slice(),
Slice{witness.signature.data(), witness.signature.size()}))
return tefFAILURE;
}

View File

@@ -1,12 +1,18 @@
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/ValidatorBitset.h>
#include <algorithm>
#include <set>
#include <stdexcept>
#include <utility>
#include <vector>
namespace ripple {
namespace ExportResultBuilder {
@@ -15,6 +21,50 @@ namespace {
constexpr std::size_t maxTxnSignatureBytes =
ExportLimits::maxExportSignatureBytes - 32 - 33;
struct BuiltWitnessSignatures
{
STArray entries;
Blob contributors;
};
BuiltWitnessSignatures
buildWitnessSignatures(
PositionedSignatureSnapshot const& signatures,
std::size_t const universeSize)
{
if (universeSize == 0 ||
universeSize > ExportLimits::maxValidatorUniverseMembers ||
signatures.empty() ||
signatures.size() > ExportLimits::maxCommitteeMembers)
Throw<std::invalid_argument>("invalid Export witness dimensions");
STArray entries(sfExportSigners);
Blob contributors(validatorBitsetBytes(universeSize), 0);
std::set<PublicKey> signingKeys;
hash_set<AccountID> signerAccounts;
for (auto const& [position, witness] : signatures)
{
if (position >= universeSize || witness.signature.empty() ||
witness.signature.size() > maxTxnSignatureBytes ||
!signingKeys.insert(witness.signingKey).second ||
!signerAccounts.insert(calcAccountID(witness.signingKey)).second)
Throw<std::invalid_argument>("invalid Export witness signer");
contributors[position / 8] |=
static_cast<std::uint8_t>(1u << (position % 8));
STObject entry(sfExportSigner);
entry.setFieldVL(sfSigningPubKey, witness.signingKey.slice());
entry.setFieldVL(
sfTxnSignature,
Slice{witness.signature.data(), witness.signature.size()});
entries.push_back(std::move(entry));
}
return {std::move(entries), std::move(contributors)};
}
STArray
buildSigners(SignatureSnapshot const& signatures, bool capForTargetChain)
{
@@ -107,60 +157,91 @@ STTx
buildSignatureWitness(
uint256 const& exportTxHash,
STTx const& releaseTarget,
SignatureSnapshot const& signatures,
Blob const& contributors,
PositionedSignatureSnapshot const& signatures,
std::size_t const universeSize,
LedgerIndex currentSeq)
{
auto const assembled =
buildMultiSignedExportedTxn(releaseTarget, signatures);
auto witnessSignatures = buildWitnessSignatures(signatures, universeSize);
auto const target = normalizeAuthorizationEnvelope(releaseTarget);
return STTx(ttEXPORT_SIGNATURES, [&](auto& obj) {
obj.setFieldU32(sfLedgerSequence, currentSeq);
obj.setAccountID(sfAccount, AccountID{});
obj.setFieldU32(sfSequence, 0);
obj.setFieldAmount(sfFee, STAmount{});
obj.setFieldH256(sfTransactionHash, exportTxHash);
obj.set(std::make_unique<STObject>(assembled));
obj.setFieldVL(sfEntropyContributors, contributors);
obj.set(std::make_unique<STObject>(target));
obj.setFieldVL(sfEntropyContributors, witnessSignatures.contributors);
obj.setFieldArray(
sfExportSigners, std::move(witnessSignatures.entries));
});
}
std::optional<SignatureSnapshot>
std::optional<PositionedSignatureSnapshot>
signaturesFromWitness(STTx const& witness)
{
if (witness.getTxnType() != ttEXPORT_SIGNATURES ||
!witness.isFieldPresent(sfExportedTxn))
!witness.isFieldPresent(sfExportedTxn) ||
!witness.isFieldPresent(sfEntropyContributors) ||
!witness.isFieldPresent(sfExportSigners))
return std::nullopt;
auto const& exported = const_cast<STTx&>(witness)
.peekAtField(sfExportedTxn)
.downcast<STObject>();
if (!exported.isFieldPresent(sfSigners))
if (exported.isFieldPresent(sfTxnSignature) ||
exported.isFieldPresent(sfSigners) ||
!exported.isFieldPresent(sfSigningPubKey) ||
!exported.getFieldVL(sfSigningPubKey).empty())
return std::nullopt;
SignatureSnapshot signatures;
for (auto const& signer : exported.getFieldArray(sfSigners))
auto const& contributors = witness.getFieldVL(sfEntropyContributors);
auto const& entries = witness.getFieldArray(sfExportSigners);
if (contributors.empty() ||
contributors.size() > ExportLimits::maxCommitteeMaskBytes ||
entries.empty() || entries.size() > ExportLimits::maxCommitteeMembers)
return std::nullopt;
std::vector<std::uint16_t> positions;
positions.reserve(entries.size());
for (std::size_t byte = 0; byte < contributors.size(); ++byte)
{
if (signer.getFName() != sfSigner ||
!signer.isFieldPresent(sfAccount) ||
!signer.isFieldPresent(sfSigningPubKey) ||
!signer.isFieldPresent(sfTxnSignature))
for (std::size_t bit = 0; bit < 8; ++bit)
{
if ((contributors[byte] & static_cast<std::uint8_t>(1u << bit)) !=
0)
positions.push_back(static_cast<std::uint16_t>(byte * 8 + bit));
}
}
if (positions.size() != entries.size())
return std::nullopt;
PositionedSignatureSnapshot signatures;
std::set<PublicKey> signingKeys;
hash_set<AccountID> signerAccounts;
for (std::size_t i = 0; i < entries.size(); ++i)
{
auto const& entry = entries[i];
if (entry.getFName() != sfExportSigner || entry.getCount() != 2 ||
!entry.isFieldPresent(sfSigningPubKey) ||
!entry.isFieldPresent(sfTxnSignature))
return std::nullopt;
auto const pkBlob = signer.getFieldVL(sfSigningPubKey);
auto const pkBlob = entry.getFieldVL(sfSigningPubKey);
if (!publicKeyType(makeSlice(pkBlob)))
return std::nullopt;
if (signer.getAccountID(sfAccount) !=
calcAccountID(PublicKey(makeSlice(pkBlob))))
PublicKey const signingKey{makeSlice(pkBlob)};
if (!signingKeys.insert(signingKey).second ||
!signerAccounts.insert(calcAccountID(signingKey)).second)
return std::nullopt;
auto const sigBlob = signer.getFieldVL(sfTxnSignature);
auto const sigBlob = entry.getFieldVL(sfTxnSignature);
if (sigBlob.empty() || sigBlob.size() > maxTxnSignatureBytes)
return std::nullopt;
auto const [_, inserted] = signatures.emplace(
PublicKey(makeSlice(pkBlob)),
Buffer(sigBlob.data(), sigBlob.size()));
positions[i],
PositionedSignature{
signingKey, Buffer(sigBlob.data(), sigBlob.size())});
if (!inserted)
return std::nullopt;
}

View File

@@ -11,6 +11,7 @@
#include <xrpl/protocol/UintTypes.h>
#include <cstddef>
#include <cstdint>
#include <map>
#include <optional>
@@ -19,6 +20,15 @@ namespace ExportResultBuilder {
using SignatureSnapshot = std::map<PublicKey, Buffer>;
struct PositionedSignature
{
PublicKey signingKey;
Buffer signature;
};
using PositionedSignatureSnapshot =
std::map<std::uint16_t, PositionedSignature>;
struct SignatureWitness
{
uint256 witnessHash;
@@ -52,11 +62,11 @@ STTx
buildSignatureWitness(
uint256 const& exportTxHash,
STTx const& releaseTarget,
SignatureSnapshot const& signatures,
Blob const& contributors,
PositionedSignatureSnapshot const& signatures,
std::size_t universeSize,
LedgerIndex currentSeq);
std::optional<SignatureSnapshot>
std::optional<PositionedSignatureSnapshot>
signaturesFromWitness(STTx const& witness);
AssembledExportResult