mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-31 19:10:10 +00:00
fix(export): preserve witness signer attribution
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -634,4 +634,5 @@ TRANSACTION(ttEXPORT_SIGNATURES, 106, ExportSignatures, ({
|
||||
{sfTransactionHash, soeREQUIRED},
|
||||
{sfExportedTxn, soeREQUIRED},
|
||||
{sfEntropyContributors, soeREQUIRED},
|
||||
{sfExportSigners, soeREQUIRED},
|
||||
}))
|
||||
|
||||
@@ -53,6 +53,13 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfTxnSignature, soeREQUIRED},
|
||||
});
|
||||
|
||||
add(sfExportSigner.jsonName,
|
||||
sfExportSigner.getCode(),
|
||||
{
|
||||
{sfSigningPubKey, soeREQUIRED},
|
||||
{sfTxnSignature, soeREQUIRED},
|
||||
});
|
||||
|
||||
add(sfMajority.jsonName,
|
||||
sfMajority.getCode(),
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user