diff --git a/.testnet/scenarios/export/export_helpers.py b/.testnet/scenarios/export/export_helpers.py index 50c042e5c..34035963b 100644 --- a/.testnet/scenarios/export/export_helpers.py +++ b/.testnet/scenarios/export/export_helpers.py @@ -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 diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 9ba272134..353828cf9 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -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) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index dce9bc2d0..d0deccc2a 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -634,4 +634,5 @@ TRANSACTION(ttEXPORT_SIGNATURES, 106, ExportSignatures, ({ {sfTransactionHash, soeREQUIRED}, {sfExportedTxn, soeREQUIRED}, {sfEntropyContributors, soeREQUIRED}, + {sfExportSigners, soeREQUIRED}, })) diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 1a35ee341..8745e3cba 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -53,6 +53,13 @@ InnerObjectFormats::InnerObjectFormats() {sfTxnSignature, soeREQUIRED}, }); + add(sfExportSigner.jsonName, + sfExportSigner.getCode(), + { + {sfSigningPubKey, soeREQUIRED}, + {sfTxnSignature, soeREQUIRED}, + }); + add(sfMajority.jsonName, sfMajority.getCode(), { diff --git a/src/test/app/ExportResultBuilder_test.cpp b/src/test/app/ExportResultBuilder_test.cpp index dc77e22d6..b35d13295 100644 --- a/src/test/app/ExportResultBuilder_test.cpp +++ b/src/test/app/ExportResultBuilder_test.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -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(); 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(); + 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 } diff --git a/src/test/app/Export_test.cpp b/src/test/app/Export_test.cpp index 990cd2b9a..93b7faefc 100644 --- a/src/test/app/Export_test.cpp +++ b/src/test/app/Export_test.cpp @@ -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(); + 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(); - 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( 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(); - 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); diff --git a/src/test/app/PseudoTx_test.cpp b/src/test/app/PseudoTx_test.cpp index 79099e5d4..f9880fa1e 100644 --- a/src/test/app/PseudoTx_test.cpp +++ b/src/test/app/PseudoTx_test.cpp @@ -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; } diff --git a/src/test/consensus/ConsensusExtensions_test.cpp b/src/test/consensus/ConsensusExtensions_test.cpp index 4b6711b56..d3b7dead5 100644 --- a/src/test/consensus/ConsensusExtensions_test.cpp +++ b/src/test/consensus/ConsensusExtensions_test.cpp @@ -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( 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(); - 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 diff --git a/src/xrpld/app/consensus/ConsensusExtensions.cpp b/src/xrpld/app/consensus/ConsensusExtensions.cpp index 538bf7a40..1a371381b 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.cpp +++ b/src/xrpld/app/consensus/ConsensusExtensions.cpp @@ -2148,7 +2148,6 @@ ConsensusExtensions::agreedExportWitness( return std::nullopt; ExportWitnessMaterial material; - material.contributors.resize(validatorBitsetBytes(universeSize), 0); std::set positions; hash_set signerAccounts; bool invalid = false; @@ -2221,14 +2220,14 @@ ConsensusExtensions::agreedExportWitness( } auto const [_, inserted] = material.signatures.emplace( - key, Buffer{signature.data(), signature.size()}); + static_cast(position), + ExportResultBuilder::PositionedSignature{ + key, Buffer{signature.data(), signature.size()}}); if (!inserted) { invalid = true; return; } - material.contributors[position / 8] |= - static_cast(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(std::move(witness))); } diff --git a/src/xrpld/app/consensus/ConsensusExtensions.h b/src/xrpld/app/consensus/ConsensusExtensions.h index 28966f15a..b90afd222 100644 --- a/src/xrpld/app/consensus/ConsensusExtensions.h +++ b/src/xrpld/app/consensus/ConsensusExtensions.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -126,8 +127,7 @@ public: using ExportSignatureSnapshot = std::map; struct ExportWitnessMaterial { - ExportSignatureSnapshot signatures; - Blob contributors; + ExportResultBuilder::PositionedSignatureSnapshot signatures; }; private: diff --git a/src/xrpld/app/tx/detail/Change.cpp b/src/xrpld/app/tx/detail/Change.cpp index 7c9d049a8..1634c94c4 100644 --- a/src/xrpld/app/tx/detail/Change.cpp +++ b/src/xrpld/app/tx/detail/Change.cpp @@ -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 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(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; } diff --git a/src/xrpld/app/tx/detail/ExportResultBuilder.cpp b/src/xrpld/app/tx/detail/ExportResultBuilder.cpp index be207da0a..cbda7cecc 100644 --- a/src/xrpld/app/tx/detail/ExportResultBuilder.cpp +++ b/src/xrpld/app/tx/detail/ExportResultBuilder.cpp @@ -1,12 +1,18 @@ #include +#include #include #include #include #include #include #include +#include #include +#include +#include +#include +#include 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("invalid Export witness dimensions"); + + STArray entries(sfExportSigners); + Blob contributors(validatorBitsetBytes(universeSize), 0); + std::set signingKeys; + hash_set 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("invalid Export witness signer"); + + contributors[position / 8] |= + static_cast(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(assembled)); - obj.setFieldVL(sfEntropyContributors, contributors); + obj.set(std::make_unique(target)); + obj.setFieldVL(sfEntropyContributors, witnessSignatures.contributors); + obj.setFieldArray( + sfExportSigners, std::move(witnessSignatures.entries)); }); } -std::optional +std::optional 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(witness) .peekAtField(sfExportedTxn) .downcast(); - 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 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(1u << bit)) != + 0) + positions.push_back(static_cast(byte * 8 + bit)); + } + } + if (positions.size() != entries.size()) + return std::nullopt; + + PositionedSignatureSnapshot signatures; + std::set signingKeys; + hash_set 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; } diff --git a/src/xrpld/app/tx/detail/ExportResultBuilder.h b/src/xrpld/app/tx/detail/ExportResultBuilder.h index a9a3acf35..76bb7eb2a 100644 --- a/src/xrpld/app/tx/detail/ExportResultBuilder.h +++ b/src/xrpld/app/tx/detail/ExportResultBuilder.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -19,6 +20,15 @@ namespace ExportResultBuilder { using SignatureSnapshot = std::map; +struct PositionedSignature +{ + PublicKey signingKey; + Buffer signature; +}; + +using PositionedSignatureSnapshot = + std::map; + 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 +std::optional signaturesFromWitness(STTx const& witness); AssembledExportResult