mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-23 23:20:12 +00:00
export: close committee review gaps
This commit is contained in:
@@ -111,22 +111,22 @@ def export_authority(ctx, *, require_unl_report=True, committee_node_ids=None):
|
||||
"""Build an account-owned committee declaration for a direct Export."""
|
||||
ledger_result = ctx.ledger("validated") or {}
|
||||
ledger = ledger_result.get("ledger", {})
|
||||
universe_hash = ledger_result.get("ledger_hash") or ledger.get("hash")
|
||||
if not universe_hash:
|
||||
ledger_hash = ledger_result.get("ledger_hash") or ledger.get("hash")
|
||||
if not ledger_hash:
|
||||
raise AssertionError(f"Validated ledger hash unavailable: {ledger_result}")
|
||||
|
||||
report = (
|
||||
ctx.rpc.request(
|
||||
0,
|
||||
"ledger_entry",
|
||||
{"index": _unl_report_index(), "ledger_hash": universe_hash},
|
||||
{"index": _unl_report_index(), "ledger_hash": ledger_hash},
|
||||
)
|
||||
or {}
|
||||
)
|
||||
active = report.get("node", {}).get("ActiveValidators", [])
|
||||
if not active:
|
||||
if require_unl_report:
|
||||
raise AssertionError(f"UNLReport active universe unavailable: {report}")
|
||||
raise AssertionError(f"UNLReport active validators unavailable: {report}")
|
||||
# The negative scenario still submits a structurally valid roster so
|
||||
# source eligibility, rather than client construction, rejects it.
|
||||
masters = _validator_master_keys_by_node(ctx)
|
||||
@@ -163,6 +163,26 @@ def export_authority(ctx, *, require_unl_report=True, committee_node_ids=None):
|
||||
return _export_committee_fields(selected_keys)
|
||||
|
||||
|
||||
async def create_export_committee(
|
||||
ctx, log, wallet, *, committee_node_ids=None
|
||||
):
|
||||
"""Create one immutable account-owned committee and return its fields."""
|
||||
authority = export_authority(ctx, committee_node_ids=committee_node_ids)
|
||||
result = await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "Export",
|
||||
"ExportCommittee": authority["ExportCommittee"],
|
||||
"Fee": "1000000",
|
||||
},
|
||||
wallet,
|
||||
)
|
||||
meta = result.get("meta", result.get("metaData", {}))
|
||||
if meta.get("TransactionResult") != "tesSUCCESS":
|
||||
raise AssertionError(f"Export committee setup failed: {result}")
|
||||
log(f"Export committee created: {authority['ExportCommitteeHash']}")
|
||||
return authority
|
||||
|
||||
|
||||
def find_export_signature_witness(ctx, seq, origin_hash):
|
||||
"""Find a later-ledger ExportSignatures witness for an Export origin."""
|
||||
result = ctx.ledger(seq, transactions=True)
|
||||
@@ -261,7 +281,7 @@ async def submit_direct_export(
|
||||
|
||||
tx_hash = result.get("hash") or result.get("tx_json", {}).get("hash")
|
||||
if not tx_hash:
|
||||
raise AssertionError(f"Universe mismatch missing tx hash: {result}")
|
||||
raise AssertionError(f"Committee eligibility failure missing tx hash: {result}")
|
||||
validated = result
|
||||
if not result.get("validated"):
|
||||
validated = await wait_for_validated_transaction(
|
||||
|
||||
@@ -15,6 +15,7 @@ from export_helpers import (
|
||||
dst_param,
|
||||
assert_hook_accepted,
|
||||
assert_export_latch,
|
||||
create_export_committee,
|
||||
wait_for_export_signature_witness,
|
||||
)
|
||||
|
||||
@@ -25,7 +26,7 @@ XPORT_HOOK_C = r"""
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
extern int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t committee_hash_ptr, uint32_t committee_hash_len);
|
||||
extern int64_t xport_reserve(uint32_t count);
|
||||
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
|
||||
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
|
||||
@@ -118,7 +119,9 @@ int64_t hook(uint32_t reserved) {
|
||||
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
|
||||
|
||||
uint8_t hash[32];
|
||||
int64_t xport_result = xport(SBUF(hash), (uint32_t)tx, buf - tx);
|
||||
static const uint8_t committee_hash[32] = { COMMITTEE_HASH_BYTES };
|
||||
int64_t xport_result = xport(
|
||||
SBUF(hash), (uint32_t)tx, buf - tx, SBUF(committee_hash));
|
||||
ASSERT(xport_result == 32);
|
||||
|
||||
return accept(0, 0, 0);
|
||||
@@ -137,8 +140,13 @@ async def scenario(ctx, log):
|
||||
alice = ctx.account("alice")
|
||||
carol = ctx.account("carol")
|
||||
|
||||
authority = await create_export_committee(ctx, log, alice.wallet)
|
||||
committee_bytes = bytes.fromhex(authority["ExportCommitteeHash"])
|
||||
committee_initializer = ", ".join(f"0x{byte:02X}" for byte in committee_bytes)
|
||||
hook_source = XPORT_HOOK_C.replace("COMMITTEE_HASH_BYTES", committee_initializer)
|
||||
|
||||
# Compile and install xport hook on alice
|
||||
wasm = ctx.compile_hook(XPORT_HOOK_C, label="xport")
|
||||
wasm = ctx.compile_hook(hook_source, label="xport")
|
||||
await ctx.submit_and_wait(
|
||||
{
|
||||
"TransactionType": "SetHook",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpl/protocol/ExportCommittee.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
@@ -469,6 +470,40 @@ public:
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testExportCommitteeCleanup(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
testcase("Export committee cleanup");
|
||||
|
||||
Env env{*this, features | featureExport};
|
||||
Account const alice("alice");
|
||||
Account const becky("becky");
|
||||
env.fund(XRP(10000), alice, becky);
|
||||
env.close();
|
||||
|
||||
auto const roster = serializeExportCommittee({alice.pk()});
|
||||
auto const digest = exportCommitteeHash(makeSlice(roster));
|
||||
Json::Value setup;
|
||||
setup[jss::TransactionType] = "Export";
|
||||
setup[jss::Account] = alice.human();
|
||||
setup[sfExportCommittee.jsonName] = strHex(roster);
|
||||
env(setup);
|
||||
env.close();
|
||||
|
||||
auto const committeeKey = keylet::exportCommittee(alice.id(), digest);
|
||||
BEAST_EXPECT(env.closed()->exists(committeeKey));
|
||||
|
||||
incLgrSeqForAccDel(env, alice);
|
||||
auto const acctDelFee{drops(env.current()->fees().increment)};
|
||||
env(acctdelete(alice, becky), fee(acctDelFee));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.closed()->exists(keylet::account(alice.id())));
|
||||
BEAST_EXPECT(!env.closed()->exists(committeeKey));
|
||||
}
|
||||
|
||||
void
|
||||
testResurrection(FeatureBitset features)
|
||||
{
|
||||
@@ -1279,6 +1314,7 @@ public:
|
||||
testBasics(features);
|
||||
testDirectories(features);
|
||||
testOwnedTypes(features);
|
||||
testExportCommitteeCleanup(features);
|
||||
testResurrection(features);
|
||||
testAmendmentEnable(features);
|
||||
testTooManyOffers(features);
|
||||
|
||||
@@ -117,16 +117,14 @@ class ExportResultBuilder_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
testAssemblesSignedMetadata()
|
||||
testAssemblesMultiSignedTransaction()
|
||||
{
|
||||
testcase("assembles signed metadata");
|
||||
testcase("assembles multisigned transaction");
|
||||
|
||||
auto const signerA = randomKeyPair(KeyType::secp256k1);
|
||||
auto const signerB = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(signerA.first), calcAccountID(signerB.first));
|
||||
auto const exportTxHash = makeHash("outer-export-tx");
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
signatures.emplace(
|
||||
signerA.first,
|
||||
@@ -137,16 +135,9 @@ public:
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signerB.first, signerB.second));
|
||||
|
||||
auto assembled = ExportResultBuilder::assembleDirect(
|
||||
innerTx, signatures, 123, exportTxHash);
|
||||
|
||||
BEAST_EXPECT(assembled.signerCount == 2);
|
||||
BEAST_EXPECT(assembled.metadata.getFieldU32(sfLedgerSequence) == 123);
|
||||
BEAST_EXPECT(
|
||||
assembled.metadata.getFieldH256(sfTransactionHash) == exportTxHash);
|
||||
|
||||
auto const& multiSigned =
|
||||
assembled.metadata.peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
auto const multiSigned =
|
||||
ExportResultBuilder::buildMultiSignedExportedTxn(
|
||||
innerTx, signatures);
|
||||
BEAST_EXPECT(multiSigned.getFieldVL(sfSigningPubKey).empty());
|
||||
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
|
||||
|
||||
@@ -169,42 +160,14 @@ public:
|
||||
BEAST_EXPECT(verify(pk, sigData.slice(), makeSlice(sigVL)));
|
||||
}
|
||||
|
||||
BEAST_EXPECT(
|
||||
assembled.signedTxHash ==
|
||||
multiSigned.getHash(HashPrefix::transactionID));
|
||||
auto const signedTxHash =
|
||||
multiSigned.getHash(HashPrefix::transactionID);
|
||||
|
||||
Serializer serialized;
|
||||
multiSigned.add(serialized);
|
||||
SerialIter sit(serialized.slice());
|
||||
STTx signedTx{std::ref(sit)};
|
||||
BEAST_EXPECT(signedTx.getTransactionID() == assembled.signedTxHash);
|
||||
}
|
||||
|
||||
void
|
||||
testSkipsEmptySignatures()
|
||||
{
|
||||
testcase("skips empty signatures");
|
||||
|
||||
auto const signer = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(signer.first), calcAccountID(dst.first));
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
signatures.emplace(signer.first, Buffer{});
|
||||
|
||||
auto assembled = ExportResultBuilder::assembleDirect(
|
||||
innerTx, signatures, 456, makeHash("empty-sig-export"));
|
||||
|
||||
BEAST_EXPECT(assembled.signerCount == 0);
|
||||
|
||||
auto const& multiSigned =
|
||||
assembled.metadata.peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
BEAST_EXPECT(multiSigned.getFieldVL(sfSigningPubKey).empty());
|
||||
BEAST_EXPECT(!multiSigned.isFieldPresent(sfSigners));
|
||||
BEAST_EXPECT(
|
||||
assembled.signedTxHash ==
|
||||
multiSigned.getHash(HashPrefix::transactionID));
|
||||
BEAST_EXPECT(signedTx.getTransactionID() == signedTxHash);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -336,7 +299,7 @@ public:
|
||||
calcAccountID(src.first), calcAccountID(dst.first));
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
while (signatures.size() < STTx::maxMultiSigners())
|
||||
while (signatures.size() < STTx::maxMultiSigners() + 4)
|
||||
{
|
||||
auto const signer = randomKeyPair(KeyType::secp256k1);
|
||||
signatures.emplace(
|
||||
@@ -345,13 +308,9 @@ public:
|
||||
innerTx, signer.first, signer.second));
|
||||
}
|
||||
|
||||
auto assembled = ExportResultBuilder::assembleDirect(
|
||||
innerTx, signatures, 789, makeHash("many-sig-export"));
|
||||
|
||||
BEAST_EXPECT(assembled.signerCount == STTx::maxMultiSigners());
|
||||
|
||||
auto const& multiSigned =
|
||||
assembled.metadata.peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
auto const multiSigned =
|
||||
ExportResultBuilder::buildMultiSignedExportedTxn(
|
||||
innerTx, signatures);
|
||||
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
|
||||
|
||||
if (multiSigned.isFieldPresent(sfSigners))
|
||||
@@ -365,41 +324,6 @@ public:
|
||||
signers[i].getAccountID(sfAccount));
|
||||
}
|
||||
}
|
||||
|
||||
BEAST_EXPECT(
|
||||
assembled.signedTxHash ==
|
||||
multiSigned.getHash(HashPrefix::transactionID));
|
||||
}
|
||||
|
||||
void
|
||||
testAssemblesWitnessReferenceMetadata()
|
||||
{
|
||||
testcase("assembles witness-reference metadata");
|
||||
|
||||
auto const signer = randomKeyPair(KeyType::secp256k1);
|
||||
auto const dst = randomKeyPair(KeyType::secp256k1);
|
||||
auto const innerTx = makeExportedPayment(
|
||||
calcAccountID(signer.first), calcAccountID(dst.first));
|
||||
auto const exportTxHash = makeHash("outer-export-reference");
|
||||
auto const witnessHash = makeHash("export-signature-witness");
|
||||
|
||||
ExportResultBuilder::SignatureSnapshot signatures;
|
||||
signatures.emplace(
|
||||
signer.first,
|
||||
ExportResultBuilder::signExportedTxn(
|
||||
innerTx, signer.first, signer.second));
|
||||
|
||||
auto assembled = ExportResultBuilder::assembleClosedLedger(
|
||||
innerTx, signatures, 321, exportTxHash, witnessHash);
|
||||
|
||||
BEAST_EXPECT(assembled.signerCount == 1);
|
||||
BEAST_EXPECT(assembled.metadata.getFieldU32(sfLedgerSequence) == 321);
|
||||
BEAST_EXPECT(
|
||||
assembled.metadata.getFieldH256(sfTransactionHash) == exportTxHash);
|
||||
BEAST_EXPECT(
|
||||
assembled.metadata.getFieldH256(sfExportSignatureHash) ==
|
||||
witnessHash);
|
||||
BEAST_EXPECT(!assembled.metadata.isFieldPresent(sfExportedTxn));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -812,12 +736,10 @@ public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testAssemblesSignedMetadata();
|
||||
testSkipsEmptySignatures();
|
||||
testAssemblesMultiSignedTransaction();
|
||||
testBuildMultiSignedExportedTxnDirect();
|
||||
testExportIntentHashIgnoresSignerSubset();
|
||||
testCapsSignerArray();
|
||||
testAssemblesWitnessReferenceMetadata();
|
||||
testSignatureWitnessRoundTrip();
|
||||
testSparseSignatureWitnessRoundTrip();
|
||||
testDestinationSignerOrderIsIndependent();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <xrpld/app/tx/detail/ApplyContext.h>
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/ExportCommittee.h>
|
||||
#include <xrpl/protocol/InnerObjectFormats.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
@@ -1234,6 +1235,29 @@ class Invariants_test : public beast::unit_test::suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
void
|
||||
testValidExportCommittee()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
|
||||
testcase << "ValidExportCommittee";
|
||||
|
||||
doInvariantCheck(
|
||||
{{"Invariant failed: malformed or mutated Export committee"}},
|
||||
[](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
auto const roster = serializeExportCommittee({A2.pk()});
|
||||
auto const digest = exportCommitteeHash(makeSlice(roster));
|
||||
auto sle = std::make_shared<SLE>(
|
||||
keylet::exportCommittee(A1.id(), digest));
|
||||
sle->setAccountID(sfAccount, A1.id());
|
||||
sle->setFieldH256(sfExportCommitteeHash, uint256{});
|
||||
sle->setFieldVL(sfExportCommittee, roster);
|
||||
sle->setFieldU64(sfOwnerNode, 0);
|
||||
ac.view().insert(sle);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testLockedBalance()
|
||||
{
|
||||
@@ -1288,6 +1312,7 @@ public:
|
||||
testValidNewAccountRoot();
|
||||
testNFTokenPageInvariants();
|
||||
testPermissionedDomainInvariants();
|
||||
testValidExportCommittee();
|
||||
testLockedBalance();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -72,6 +72,17 @@ public:
|
||||
BEAST_EXPECT(result[jss::result][jss::status] == "success");
|
||||
}
|
||||
|
||||
{
|
||||
auto const& exportFlags =
|
||||
result[jss::result][jss::TRANSACTION_FLAGS]["Export"];
|
||||
BEAST_EXPECT(
|
||||
exportFlags["tfExportEraseLatch"].asUInt() ==
|
||||
tfExportEraseLatch);
|
||||
BEAST_EXPECT(
|
||||
exportFlags["tfExportEraseCommittee"].asUInt() ==
|
||||
tfExportEraseCommittee);
|
||||
}
|
||||
|
||||
// check exception SFields
|
||||
{
|
||||
auto const fieldExists = [&](std::string name) {
|
||||
|
||||
@@ -1998,153 +1998,6 @@ ConsensusExtensions::clearAcceptedExportSigSet()
|
||||
acceptedExportSigSetHash_.reset();
|
||||
}
|
||||
|
||||
std::optional<ConsensusExtensions::ExportSignatureSnapshot>
|
||||
ConsensusExtensions::agreedExportSignatures(
|
||||
STTx const& exportTx,
|
||||
uint256 const& txHash,
|
||||
std::size_t threshold) const
|
||||
{
|
||||
// A local exportSigSetMap_ is only candidate material until the sidecar
|
||||
// gate accepts its root. Without this guard, a timed-out node with a local
|
||||
// partial-but-quorum map could mint a different signed export blob from
|
||||
// the quorum-aligned nodes.
|
||||
if (!acceptedExportSigSetHash_)
|
||||
{
|
||||
JLOG(j_.warn()) << "Export: exportSigSet not accepted"
|
||||
<< " txHash=" << txHash << " threshold=" << threshold;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto const acceptedHash = *acceptedExportSigSetHash_;
|
||||
std::shared_ptr<SHAMap> agreedMap;
|
||||
if (exportSigSetMap_ &&
|
||||
exportSigSetMap_->getHash().as_uint256() == acceptedHash)
|
||||
{
|
||||
agreedMap = exportSigSetMap_;
|
||||
}
|
||||
else
|
||||
{
|
||||
agreedMap = app_.getInboundTransactions().getSet(acceptedHash, false);
|
||||
}
|
||||
|
||||
if (!agreedMap)
|
||||
{
|
||||
JLOG(j_.warn()) << "Export: accepted exportSigSet missing"
|
||||
<< " acceptedHash=" << acceptedHash
|
||||
<< " txHash=" << txHash << " threshold=" << threshold;
|
||||
return std::nullopt;
|
||||
}
|
||||
if (agreedMap->mapType() != SHAMapType::SIDECAR)
|
||||
{
|
||||
JLOG(j_.warn()) << "Export: accepted exportSigSet has wrong map type"
|
||||
<< " acceptedHash=" << acceptedHash
|
||||
<< " txHash=" << txHash;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto const agreedHash = agreedMap->getHash().as_uint256();
|
||||
if (agreedHash != acceptedHash)
|
||||
{
|
||||
JLOG(j_.warn()) << "Export: accepted exportSigSet hash mismatch"
|
||||
<< " setHash=" << agreedHash
|
||||
<< " acceptedHash=" << acceptedHash
|
||||
<< " txHash=" << txHash;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// The accepted root is the membership decision. Candidate construction
|
||||
// filters live active signers, but materialization must not re-resolve
|
||||
// signer keys through mutable manifests or nodes can diverge after a
|
||||
// rotation. Keep cryptographic verification below; drop only the live
|
||||
// membership re-filter.
|
||||
ExportSignatureSnapshot signatures;
|
||||
bool invalid = false;
|
||||
agreedMap->visitLeaves(
|
||||
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
if (invalid)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
auto admitted = admitSidecarLeaf(
|
||||
item->key(),
|
||||
item->slice(),
|
||||
agreedHash,
|
||||
j_,
|
||||
"Export",
|
||||
"exportSigSet",
|
||||
"agreed",
|
||||
ExportLimits::maxExportSignatureSidecarBytes,
|
||||
&invalid);
|
||||
if (!admitted || admitted->type != sidecarExportSig)
|
||||
return;
|
||||
auto const& sidecar = admitted->sidecar;
|
||||
|
||||
if (!sidecar.isFieldPresent(sfTransactionHash) ||
|
||||
!sidecar.isFieldPresent(sfSigningPubKey) ||
|
||||
!sidecar.isFieldPresent(sfTxnSignature))
|
||||
return;
|
||||
|
||||
if (sidecar.getFieldH256(sfTransactionHash) != txHash)
|
||||
return;
|
||||
|
||||
auto const pk = sidecar.getFieldVL(sfSigningPubKey);
|
||||
if (!publicKeyType(makeSlice(pk)))
|
||||
return;
|
||||
|
||||
PublicKey const valPK{makeSlice(pk)};
|
||||
|
||||
auto const sigVL = sidecar.getFieldVL(sfTxnSignature);
|
||||
auto const sigSlice = makeSlice(sigVL);
|
||||
if (!verifyExportSignatureAgainstTx(
|
||||
exportTx,
|
||||
valPK,
|
||||
sigSlice,
|
||||
txHash,
|
||||
j_,
|
||||
"agreed exportSigSet"))
|
||||
{
|
||||
invalid = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Buffer sigBuf(sigSlice.data(), sigSlice.size());
|
||||
if (auto const [_, inserted] =
|
||||
signatures.emplace(valPK, std::move(sigBuf));
|
||||
!inserted)
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
<< "Export: accepted exportSigSet duplicate signer"
|
||||
<< " setHash=" << agreedHash << " txHash=" << txHash
|
||||
<< " signer=" << toBase58(TokenType::NodePublic, valPK);
|
||||
invalid = true;
|
||||
}
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j_.warn())
|
||||
<< "Export: agreed exportSigSet parse failed"
|
||||
<< " setHash=" << agreedHash << " txHash=" << txHash
|
||||
<< " error=" << e.what();
|
||||
invalid = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (invalid)
|
||||
return std::nullopt;
|
||||
|
||||
if (signatures.size() < threshold)
|
||||
{
|
||||
JLOG(j_.info()) << "Export: accepted exportSigSet below quorum"
|
||||
<< " setHash=" << agreedHash << " txHash=" << txHash
|
||||
<< " signers=" << signatures.size()
|
||||
<< " threshold=" << threshold;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return signatures;
|
||||
}
|
||||
|
||||
std::optional<ConsensusExtensions::ExportWitnessMaterial>
|
||||
ConsensusExtensions::agreedExportWitness(
|
||||
STTx const& exportSigningPayload,
|
||||
|
||||
@@ -128,7 +128,6 @@ public:
|
||||
|
||||
using ActiveValidatorView = ripple::ActiveValidatorView;
|
||||
using ActiveValidatorViewPtr = std::shared_ptr<ActiveValidatorView const>;
|
||||
using ExportSignatureSnapshot = std::map<PublicKey, Buffer>;
|
||||
struct ExportWitnessMaterial
|
||||
{
|
||||
ExportResultBuilder::PositionedSignatureSnapshot signatures;
|
||||
@@ -426,12 +425,6 @@ public:
|
||||
void
|
||||
clearAcceptedExportSigSet();
|
||||
|
||||
std::optional<ExportSignatureSnapshot>
|
||||
agreedExportSignatures(
|
||||
STTx const& exportTx,
|
||||
uint256 const& txHash,
|
||||
std::size_t threshold) const;
|
||||
|
||||
std::optional<ExportWitnessMaterial>
|
||||
agreedExportWitness(
|
||||
STTx const& exportSigningPayload,
|
||||
|
||||
@@ -5,7 +5,7 @@ ConsensusEntropy/RNG, proposal sidecars, and export signature convergence.
|
||||
Read this before changing `ConsensusExtensions`, `ConsensusExtensionsTick`,
|
||||
`ExtendedPosition`, sidecar SHAMap handling, or the related CSF tests.
|
||||
|
||||
The short version: extension data may coordinate extra same-ledger features,
|
||||
The short version: extension data may coordinate extra ledger-visible features,
|
||||
but it must not redefine ordinary transaction-set consensus. When extension
|
||||
state cannot be made safe in time, the extension degrades deterministically
|
||||
and the ledger still closes.
|
||||
@@ -16,7 +16,7 @@ coordination step can avoid it. Fast means those coordination steps stay short
|
||||
and conditional, never becoming an open-ended wait for an extension feature to
|
||||
succeed. Works means missed or late extension material follows that feature's
|
||||
deterministic fallback, such as Tier 1 consensus_fallback entropy for RNG or normal
|
||||
Export retry/expiry, rather than blocking core consensus.
|
||||
Export pending/expiry behavior, rather than blocking core consensus.
|
||||
|
||||
## Fallback Semantics
|
||||
|
||||
@@ -70,8 +70,8 @@ sidecar gate has had its bounded chance to use proofed/quorum material.
|
||||
they must not block ledger close indefinitely. If RNG cannot establish
|
||||
an accepted entropy set, it injects the deterministic Tier 1
|
||||
consensus_fallback digest (labeled `consensus_fallback`, count 0). If export
|
||||
signatures cannot converge, export retries or expires according to
|
||||
transaction rules.
|
||||
signatures cannot converge, the Export remains pending or its publication
|
||||
window expires without blocking ledger close.
|
||||
|
||||
The bounded fallback rule is not permission for local shortcuts to decide
|
||||
ledger output. "Cannot establish" means the accepted-hash gate did not
|
||||
@@ -140,7 +140,8 @@ sidecar gate has had its bounded chance to use proofed/quorum material.
|
||||
|
||||
## Validator Set And Quorum
|
||||
|
||||
The active validator view is the shared denominator for RNG and export:
|
||||
The active validator view is the shared source-alignment denominator for RNG
|
||||
and Export qV. Export qC has a separate account-owned committee denominator:
|
||||
|
||||
- Prefer `UNLReport.sfActiveValidators` from the consensus parent ledger.
|
||||
- If no report is available, fall back to configured trusted validators so
|
||||
@@ -214,7 +215,8 @@ served, fetched, or merged from peers, and generic transaction-set acquisition
|
||||
must reject them. A node that missed proposal-carried material may therefore be
|
||||
unable to materialize the quorum root this round. For RNG it falls back to the
|
||||
explicit Tier 1 consensus digest or accepts a lower locally materialized tier; for
|
||||
Export the transaction retries or expires. If a quorum of validators did
|
||||
Export the latch remains pending or its publication window expires. If a quorum
|
||||
of validators did
|
||||
materialize and validate a richer synthetic ledger, a missing-material validator
|
||||
follows that ledger through the normal validation/LCL path after the round, just
|
||||
as it would after failing to build any other majority ledger.
|
||||
@@ -403,7 +405,7 @@ Sidecar SHAMaps are local immutable snapshots:
|
||||
- Peer-advertised roots are alignment evidence, not payload availability.
|
||||
- Nodes never fetch, advertise, serve, or merge sidecar maps from peers.
|
||||
- If a quorum root cannot be materialized locally before the bounded deadline,
|
||||
RNG degrades/falls back and Export retries/expires.
|
||||
RNG degrades/falls back and Export injects no witness in that ledger.
|
||||
|
||||
Do not use avalanche-style transaction inclusion logic for sidecar inputs.
|
||||
For RNG and export sidecars, the disagreement to resolve is usually timing or
|
||||
@@ -426,50 +428,37 @@ sidecar memory.
|
||||
`featureExport` and `featureConsensusEntropy` are independently amendment
|
||||
gated.
|
||||
|
||||
Export can run without ConsensusEntropy and still uses the active validator
|
||||
view's 80% quorum threshold. Verified export signature sidecars converge
|
||||
through `ExtendedPosition`, and the `exportSigSetHash` is signed by proposals
|
||||
whether or not RNG is enabled. Do not make Export liveness depend on unanimity:
|
||||
one active validator with a missing, delayed, or conflicting sidecar must not
|
||||
veto an otherwise quorum-aligned export round.
|
||||
Export can run without ConsensusEntropy. Two thresholds remain deliberately
|
||||
separate:
|
||||
|
||||
Non-standalone Export completion requires a UNLReport-backed active validator
|
||||
view. If the parent ledger has no `UNLReport`, Export has no safe deterministic
|
||||
fallback result, so validators do not publish target-chain signature shares and
|
||||
the export retries or expires rather than finalizing against local
|
||||
trusted-configuration thresholds.
|
||||
- qC is `ceil(0.8 * C)` over one explicit account-owned committee of at most 32
|
||||
validator master keys. It decides whether one Export has enough target
|
||||
signatures.
|
||||
- qV is the source active-validator threshold used to align one exact
|
||||
`exportSigSetHash`. It decides whether the sidecar snapshot is coordinated
|
||||
enough to materialize witnesses.
|
||||
|
||||
Export's original pre-NegativeUNL validator population must also fit the
|
||||
32-member target serialization bound. Validators publish no target-chain shares
|
||||
and Export cannot materialize while it exceeds that bound; temporary NegativeUNL
|
||||
filtering must not select an implicit bridge committee.
|
||||
One missing sidecar advertiser must not veto a qV-aligned root, and one missing
|
||||
committee signer must not lower qC. Neither threshold is unanimity or a count of
|
||||
locally visible peers.
|
||||
|
||||
The bounded deployment contract then mirrors the source validator-derived key
|
||||
universe, weights, and Export/validation threshold in the destination account's
|
||||
SignerList. A lower destination threshold permits target execution before the
|
||||
authority needed for source materialization exists. A higher threshold preserves
|
||||
that safety direction but can strand a successful source latch. The destination
|
||||
network's ledger-validation quorum is separate: it validates the authorized
|
||||
transaction's containing ledger, and XPOP proves that finality on return. A
|
||||
static destination SignerList should remain anchored to the original source
|
||||
universe during NegativeUNL periods, trading Export liveness for unchanged
|
||||
destination authority.
|
||||
The committee is an immutable `ltEXPORT_COMMITTEE` owned by the exporting
|
||||
account. It stores one canonical sorted master-key roster and is named by a
|
||||
network-neutral content digest. Latches store only the digest. There is no
|
||||
default committee and no implicit capped subset of the full UNL. Bare setup may
|
||||
pre-stage a roster, but every actual intent checks every roster master against
|
||||
the exact admitting parent ledger's pre-NegativeUNL `UNLReport`. Inline roster
|
||||
creation and intent admission perform that check atomically. Committee deletion
|
||||
is blocked while the owner has any live Export latch.
|
||||
|
||||
The bounded MVP deployment requires a submitter co-signer in the destination
|
||||
SignerList, held by the target-submission process. Choose weights so destination
|
||||
quorum requires both the source-validator threshold and that key; for validator
|
||||
weight total `V` and required validator weight `q`, submitter weight `V` with
|
||||
target quorum `V + q` is the simple construction. Proposal-carried validator
|
||||
shares then remain inert until the submitter observes a validated source latch
|
||||
and signs. The key cannot authorize alone but becomes a liveness and censorship
|
||||
dependency. It consumes one destination SignerList entry, so this deployment
|
||||
supports at most 31 source validator identities under a 32-entry limit.
|
||||
|
||||
The current code has no committee selector. A future committee must be explicit,
|
||||
ledger-anchored, and versioned; a locally configured or sorted UNL subset would
|
||||
reintroduce nondeterministic or unnamed authority. Full source consensus would
|
||||
still validate the Export, while committee quorum plus the submitter would be
|
||||
the honestly stated destination authority.
|
||||
The destination account's SignerList remains the actual remote authority. Its
|
||||
keys, weights, optional operator signer, and rotation policy are client/operator
|
||||
configuration that source consensus cannot verify. The committee cap matches
|
||||
`STTx::maxMultiSigners()`; reserving a separate operator entry reduces the
|
||||
maximum validator roster usable by that destination account. A lower remote
|
||||
threshold weakens the claimed custody policy, while a higher threshold may
|
||||
strand an otherwise sufficient source witness. Target ledger validation and the
|
||||
returning XPOP are separate from this authorization decision.
|
||||
|
||||
The extended proposal machinery is enabled when either feature needs signed
|
||||
sidecar fields. Do not make Export depend on RNG availability just because RNG
|
||||
@@ -488,110 +477,88 @@ negotiation before activation.
|
||||
When `featureExport` is disabled, the export sidecar gate is disabled too. Stale
|
||||
collector entries must not keep a stopped amendment active.
|
||||
|
||||
Only verified export signatures count toward quorum or enter export sidecar
|
||||
SHAMaps. Proposal-ingress signatures are sender-bound to the trusted proposal
|
||||
validator and may be stored as unverified until the matching export transaction
|
||||
is available for cryptographic verification.
|
||||
Intent admission creates an origin-keyed `ltEXPORT_LATCH` immediately. The latch
|
||||
records the normalized identity digest, origin `W` and source sequence,
|
||||
publication end, destination TicketSequence, committee digest, owner-directory
|
||||
link, and pending-work link. Lack of signatures does not make the source
|
||||
transaction retry. The pending directory is durable scheduling state across
|
||||
rounds and restarts.
|
||||
|
||||
The consensus candidate transaction set is the authority for export signature
|
||||
verification. The open ledger may be used for early proposal ingestion, but
|
||||
once a candidate tx set exists, only signatures verified against the `ttEXPORT`
|
||||
in that candidate set may become quorum material or enter `exportSigSetHash`.
|
||||
Honest signature release is keyed to fully validated history, not to the open
|
||||
ledger or current transaction candidate set. When an exact origin ledger becomes
|
||||
validated, eligible committee members reconstruct the target payload, append
|
||||
the canonical release anchor `(source domain, target domain, W, origin sequence,
|
||||
origin hash)`, and sign that target with their current manifest signing key.
|
||||
Ledger building and validation are asynchronous; a later build may already be
|
||||
in progress. Export waits for finality without waiting or rewinding base
|
||||
consensus.
|
||||
|
||||
Export sidecar publication is local-material only. A node may publish only the
|
||||
verified export signatures it actually has locally, and only for `ttEXPORT`
|
||||
transactions in the consensus candidate set. Peer-advertised export roots are
|
||||
used for quorum alignment; they do not reconstruct missing signature material.
|
||||
The accepted local snapshot root, once quorum-aligned, is the source for the
|
||||
ledger witness. A node that cannot materialize the accepted quorum witness
|
||||
locally retries or expires the export and follows the quorum ledger later through
|
||||
normal validation if other validators built the witness.
|
||||
`ttEXPORT_SIGNATURES` is the export signature witness interface. Network mode
|
||||
derives it from the accepted `exportSigSetHash` sidecar snapshot; standalone/dev
|
||||
helpers may synthesize the same witness from the local validator key. The pseudo
|
||||
carries the full source-side validator signature witness and binds it to the
|
||||
matching `ttEXPORT` via `sfTransactionHash`. It has no ledger-state effect by
|
||||
itself; it exists so apply/replay sees the same signature material through the
|
||||
transaction stream regardless of which producer supplied it.
|
||||
Only structurally valid, attributable, cryptographically verified shares enter
|
||||
the collector. A share identifies the owner, `W`, exact origin ledger, committee
|
||||
position, signing key, and ordinary XRPL multisignature. Live admission resolves
|
||||
the immutable committee master at that position through the manifest cache. A
|
||||
missing binding is deferred or rejected; it never silently selects another
|
||||
member.
|
||||
|
||||
If the consensus candidate contains a `ttEXPORT` but the node has no eligible
|
||||
local export signatures yet, the export sidecar gate opens only a bounded
|
||||
safety window for tx-converged peers to advertise `exportSigSetHash`. This is
|
||||
not a wait-for-Export-success mechanism; it is a short opportunity to avoid
|
||||
closing a minority ledger while sidecar convergence is already reachable. If no
|
||||
advertised sidecar appears by the deadline, the gate stops waiting and the
|
||||
export retries or expires through normal transaction rules.
|
||||
The dedicated `mtEXPORT_SHARES` relay is the immediate post-validation flood and
|
||||
subscription path. Proposal carriage republishes locally retained shares as a
|
||||
bounded authenticated backup. Both feed the same collector and verification
|
||||
logic. Proposal-carried material is still untrusted until the proposal and share
|
||||
semantics verify.
|
||||
|
||||
Export success requires quorum alignment on `exportSigSetHash`, not merely a
|
||||
local collector quorum. Since `featureExport` enables signed extended proposal
|
||||
fields, a quorum-aligned `exportSigSetHash` is enough to proceed even if a
|
||||
tx-converged minority peer has not advertised an export sidecar hash. Do not let
|
||||
one active validator with a missing sidecar force an otherwise quorum-aligned
|
||||
export round to retry or expire. Full observation remains useful diagnostics; it
|
||||
is not an Export success precondition. If no export signature hash reaches quorum
|
||||
alignment by the bounded deadline, do not choose the largest non-quorum set; the
|
||||
export retries or expires according to normal transaction rules.
|
||||
Closed-ledger apply consumes the pre-scanned `ttEXPORT_SIGNATURES` witness, not
|
||||
the live collector and not ephemeral sidecar state. `Export::doApply` rebuilds
|
||||
the active validator view from the parent ledger, verifies each witness
|
||||
signature against the `ttEXPORT` inner transaction, requires source-view quorum,
|
||||
then canonically assembles the target-chain multisigned transaction. In live
|
||||
consensus builds, `onPreBuild` first removes any pre-existing export witness and
|
||||
re-materializes the witness from the accepted `exportSigSetHash` root. That
|
||||
accepted transaction-stream witness supplies signer membership; apply must not
|
||||
re-resolve those signing keys through the current manifest cache, because
|
||||
manifest gossip can differ while the parent-ledger active view is the same.
|
||||
Historical `LedgerReplay` consumes the same persisted witness after manifests
|
||||
may have rotated. In both modes, apply still checks signatures and threshold. A
|
||||
node that times out before accepting a root has no witness and retries/expires;
|
||||
a node that proceeds uses the same transaction-stream witness during live build
|
||||
and historical replay. The build-scoped witness map is only an index over that
|
||||
ordered transaction stream, not hidden consensus state. This avoids
|
||||
successful-but-different export blobs while preserving the bounded wait model.
|
||||
The collector forms one complete bounded unique-share union over all live
|
||||
origins, keyed by `(W, committeePosition)`. A second distinct valid contribution
|
||||
at one position is conflicting and contributes zero for that origin. The union
|
||||
is projected into an ephemeral sidecar map; its root is advertised in signed
|
||||
`ExtendedPosition` traffic. qC is not a root filter: below-qC partial material
|
||||
still participates in the common union, while qC is checked only when deciding
|
||||
which witnesses can be materialized.
|
||||
|
||||
`sfExportResult` metadata stores `sfExportSignatureHash`, a direct reference to
|
||||
the witness pseudo, rather than duplicating the full signature payload. Clients
|
||||
assemble the final foreign-chain blob from `ttEXPORT` plus the witness
|
||||
signatures, or can use a convenience RPC/helper that performs that pure
|
||||
read-time assembly. That expansion must match `ExportResultBuilder`: canonical
|
||||
AccountID signer ordering, empty `SigningPubKey`, and the target-chain signer
|
||||
cap (`STTx::maxMultiSigners()`) before hashing or submitting. The witness can
|
||||
carry more source-side signatures than the destination transaction may include.
|
||||
Export currently refuses to sign or materialize when the original UNLReport
|
||||
validator population exceeds that cap, even if NegativeUNL temporarily shrinks
|
||||
the effective view below it. The assembly cap remains a defensive serialization
|
||||
bound, not an implicit committee-selection policy.
|
||||
Export sidecar publication is local-material only. Peer-advertised roots are qV
|
||||
alignment evidence; they do not reconstruct missing signatures. A node injects
|
||||
from an accepted root only when its exact local sidecar snapshot matches that
|
||||
root and independently verifies. The bounded gate is a short chance for
|
||||
already-in-flight material to align after ordinary transaction-set convergence,
|
||||
not a wait for any particular Export to reach qC. Timeout always closes the
|
||||
ledger and injects no Export witness from an unavailable or unaligned root.
|
||||
|
||||
The resulting Export latch stores the normalized target transaction's canonical
|
||||
signing hash in `sfDigest`, not one assembled multisigned transaction ID. Import
|
||||
therefore accepts any destination-valid signer subset for that exact signing
|
||||
intent. A missing latch returns `telEXPORT_LATCH_REQUIRED` before consensus or
|
||||
Hook execution, so an XPOP that races source materialization can be relayed
|
||||
later.
|
||||
For each still-pending origin that reaches qC in the accepted root, pre-build
|
||||
materializes at most one canonical later-ledger `ttEXPORT_SIGNATURES`. The
|
||||
pseudo carries `W`, the release-stamped target transaction, a committee-relative
|
||||
contributor bitmap, and the selected signatures. It is the replay witness for
|
||||
the accepted sidecar evidence.
|
||||
|
||||
Export-latch cancellation is non-revoking. While publication is pending it
|
||||
unlinks signing work and retains callback readiness because shares already
|
||||
published to peers cannot be withdrawn. Lifecycle control names the exact
|
||||
source issuance `W`: no flag always unlinks and retains in every state, while
|
||||
`tfExportEraseLatch` explicitly erases that exact latch in any state, releases
|
||||
reserve, and knowingly accepts later target execution without callback
|
||||
readiness. Publication expiry follows the retain path. Symmetric witness+XPOP
|
||||
completion also erases. v1 has no automatic terminal-retirement clock,
|
||||
permanent tombstone, or paid-bump transition.
|
||||
`Change::applyExportSignatures` reads the referenced immutable committee from
|
||||
parent ledger state, derives its size and qC, validates the contributor bitmap,
|
||||
normalization and release anchor, checks every target signature, records the
|
||||
witness transaction ID on the latch, and removes the pending link. It does not
|
||||
read current manifests. Historical replay consumes the same witness bytes and
|
||||
committee SLE and never regenerates shares. An explicitly erased latch makes a
|
||||
concurrently ordered witness an evidence-only no-op.
|
||||
|
||||
Implementation status: origin-keyed creation, W-only lifecycle control,
|
||||
expiry-unlink, and symmetric witness/XPOP recording are present.
|
||||
The full release-stamped payload and signatures are stored once in the witness.
|
||||
The latch stores only `sfExportSignatureHash`. Read-time assembly uses the
|
||||
witness's target and signatures, empty `SigningPubKey`, canonical AccountID
|
||||
ordering, and the target's 32-signer cap. The normalized latch digest is
|
||||
signature-subset-independent, so a different sufficient signer subset does not
|
||||
orphan the source callback.
|
||||
|
||||
This is intentionally leaner than XPOP. XPOP carries its own UNL and manifest
|
||||
bundle so it can be independently verified as an external proof. Export witnesses
|
||||
are not external proof bundles; they are inputs that made it into validated
|
||||
ledger history. Making them self-contained would require embedding manifest
|
||||
material or equivalent signing-key history in every witness, which is a separate
|
||||
protocol/storage design.
|
||||
Import/XPOP and witness arrival are independent monotonic facts. Whichever
|
||||
arrives second erases the latch and releases reserve. Flagless W-only lifecycle
|
||||
control unlinks publication but retains callback readiness; `tfExportEraseLatch`
|
||||
irreversibly erases the exact issuance. Publication expiry follows the retain
|
||||
path. Committee deletion remains blocked while any owner latch exists. v1 has
|
||||
no permanent tombstone or paid-bump transition.
|
||||
|
||||
Closed-ledger apply must not promote unverified proposal-carried signatures into
|
||||
current-round quorum material. It may verify and retain them for a future retry,
|
||||
where they can be published in a sidecar set and converged before use.
|
||||
The `export_signatures` subscription projects each admitted post-validation
|
||||
share immediately and republishes retained live shares once per validated
|
||||
cursor. Clients deduplicate by origin and committee position. Slow subscribers
|
||||
must not hold collector or consensus locks, and silence after an origin leaves
|
||||
the pending set is not a durable terminal event.
|
||||
|
||||
This remains intentionally leaner than XPOP. XRPL sees ordinary multisignatures,
|
||||
not a proof of Xahau finality. XPOP carries the reverse-chain proof material and
|
||||
drives the matching callback after target finality.
|
||||
|
||||
Export sig convergence runs in parallel with RNG. An export-side convergence
|
||||
failure must not change RNG semantics; an RNG fallback must not make export
|
||||
@@ -602,9 +569,10 @@ Accept-time cleanup must preserve Export state through `onPreBuild` whenever
|
||||
After the ordered transaction set contains that witness, replay must not need
|
||||
the round's export sidecar convergence state.
|
||||
|
||||
CSF consensus tests model the export sidecar gate directly. Testnet scenarios
|
||||
under `.testnet/scenarios/export/` cover live-node Export+CE behavior and
|
||||
Export-only quorum behavior.
|
||||
CSF consensus tests model the Export sidecar gate directly. Unit and testnet
|
||||
coverage must additionally exercise committee setup/use/deletion, post-validation
|
||||
relay and subscription, restart recovery, below-qC persistence, and later
|
||||
witness/XPOP ordering.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
@@ -627,11 +595,18 @@ When changing consensus extension code, check these questions:
|
||||
- Are sidecar entries typed as sidecars, not pseudo-transactions?
|
||||
- Are proposal-visible or validation-visible sidecar fields covered by the
|
||||
relevant signature and duplicate/replay identity?
|
||||
- Are export signatures verified before they count?
|
||||
- Does export success require `exportSigSetHash` alignment, not just local
|
||||
collector quorum?
|
||||
- Does every ledger-defining export signature enter replay as a
|
||||
`ttEXPORT_SIGNATURES` witness before `ttEXPORT` applies?
|
||||
- Can one bad validator deny Export to an honest quorum? It must not.
|
||||
- Can timeout select a largest-but-below-quorum export sidecar set? It must not.
|
||||
- Can an Export share be produced before the exact origin ledger validates? It
|
||||
must not.
|
||||
- Does every intent name an immutable account-owned committee whose complete
|
||||
roster is checked against the exact admitting parent's pre-NegativeUNL
|
||||
`UNLReport`?
|
||||
- Are qC (committee sufficiency) and qV (source root alignment) kept separate?
|
||||
- Are Export signatures attributable and verified before collector admission?
|
||||
- Does witness materialization require local possession of the exact
|
||||
qV-aligned `exportSigSetHash`, not merely a local qC?
|
||||
- Does every ledger-defining Export signature enter replay through a later
|
||||
self-contained `ttEXPORT_SIGNATURES` witness?
|
||||
- Does accepted witness apply derive qC from the immutable committee SLE without
|
||||
consulting mutable manifests?
|
||||
- Can timeout select a largest-but-below-qC signature set? It must not.
|
||||
- Are CE and Export still independently gated and independently stoppable?
|
||||
|
||||
@@ -1,200 +1,176 @@
|
||||
# Export — Design Intent (canonical spine)
|
||||
# Export - Design Intent (canonical spine)
|
||||
|
||||
This is the **normative** intent for `featureExport`: the invariants that must
|
||||
hold regardless of how the implementation is refactored. The verbose mechanics
|
||||
live in `ConsensusExtensionsDesign.md`; the reviewer-facing walkthrough lives in
|
||||
the PR description. **Both defer to this file.**
|
||||
This is the normative intent for `featureExport`: the invariants that must hold
|
||||
regardless of implementation refactoring. Detailed mechanics live in
|
||||
`ConsensusExtensionsDesign.md`; both documents must change with any deliberate
|
||||
protocol change.
|
||||
|
||||
How to use it: if code contradicts an invariant below, the *code* is wrong — or
|
||||
the invariant is being changed and **this file must be consciously edited in the
|
||||
same change, with the rationale**. In particular, Export is not replay-clean
|
||||
unless every closed-ledger effect can be rebuilt from the parent ledger and the
|
||||
closed transaction set alone.
|
||||
Export is not replay-clean unless every closed-ledger effect can be rebuilt
|
||||
from the parent ledger and ordered closed transaction set alone.
|
||||
|
||||
## Purpose (one line)
|
||||
## Purpose
|
||||
|
||||
Export lets a quorum of active validators produce a foreign-chain-submittable
|
||||
transaction from an agreed `ttEXPORT`, without letting local sidecar timing,
|
||||
collector state, or validator silence change the bytes of a successful closed
|
||||
ledger result.
|
||||
Export lets an account choose a bounded validator committee whose members sign
|
||||
an ordinary target-chain transaction only after the exact source intent has
|
||||
validated. A later source-ledger witness records the sufficient signatures.
|
||||
|
||||
## Invariants
|
||||
|
||||
**INV-1 — Quorum, not unanimity.**
|
||||
Export success is gated by active-validator quorum alignment on the export
|
||||
signature set. A missing, delayed, or silent minority must not veto an otherwise
|
||||
quorum-aligned export.
|
||||
*Anti-pattern:* requiring full observation of every tx-converged validator before
|
||||
success.
|
||||
**INV-1 - Source intent precedes authority release.**
|
||||
`ttEXPORT` first validates its target payload, committee reference, source
|
||||
account, destination TicketSequence, reserved origin Memo, and bounded timing
|
||||
fields. Successful apply creates a durable origin-keyed latch. It does not
|
||||
claim that signatures or a witness already exist.
|
||||
|
||||
**INV-2 — Fixed active-view denominator.**
|
||||
Export thresholds are computed over the parent-ledger active validator view. The
|
||||
denominator must never be derived from locally observed peers, locally available
|
||||
signatures, or the subset that happened to advertise sidecar hashes.
|
||||
On networks that use NegativeUNL, `featureNegativeUNLActiveViewCap` should be
|
||||
active before or with Export so producer-side nUNL voting caps against the same
|
||||
UNLReport active-source universe that bounds NegativeUNL shrink. Export quorum
|
||||
itself is computed over the effective post-NegativeUNL active view. Direct
|
||||
Export apply still rebuilds and defensively caps the parent active view before
|
||||
checking the witness threshold.
|
||||
*Anti-pattern:* letting silence shrink the quorum threshold.
|
||||
*Anti-pattern:* treating provisional open-ledger execution or source
|
||||
transaction-set agreement as authorization to release destination signatures.
|
||||
|
||||
**INV-3 — Accepted sidecar root, not live collector.**
|
||||
Any successful Export apply path must use the signature set rooted at the
|
||||
`exportSigSetHash` accepted by the tick gate. Late local collector arrivals,
|
||||
timeout flags, or unverified proposal-carried signatures must not change the
|
||||
signer set selected for the ledger.
|
||||
*Anti-pattern:* assembling from `ExportSigCollector` at apply time.
|
||||
**INV-2 - Committee selection is explicit and account-owned.**
|
||||
An Export committee is an immutable canonical roster of at most 32 validator
|
||||
master keys stored in an account-owned `ltEXPORT_COMMITTEE`. Its identity is a
|
||||
network-neutral content digest over that roster. A latch stores only the
|
||||
digest; committee size and qC are derived from the referenced object.
|
||||
|
||||
**INV-4 — Replay witness in the transaction stream.**
|
||||
If export sidecar material changes closed-ledger output, that material must be
|
||||
represented by canonical ledger input before apply. The closed ledger must be
|
||||
replayable from `(parent ledger, ordered closed transaction set)` without live
|
||||
consensus sidecar memory. Export signatures are such a witness: they determine
|
||||
source quorum success and exported-result metadata, so they must be carried by
|
||||
a replayable companion pseudo transaction or equivalent transaction-stream
|
||||
artifact. The Export-latch intent hash is signature-independent.
|
||||
*Anti-pattern:* using ephemeral accepted sidecar state to create an Export latch
|
||||
or result that cannot be reconstructed by ledger delta replay.
|
||||
There is no protocol-default committee. A bare setup transaction may pre-stage
|
||||
a structurally valid future roster without creating a latch or checking current
|
||||
membership. Every actual Export intent, including inline create-and-use,
|
||||
requires every roster master to appear in the exact admitting parent ledger's
|
||||
pre-NegativeUNL `UNLReport`; otherwise it returns
|
||||
`tecEXPORT_COMMITTEE_UNAVAILABLE`. A client may offer "copy current UNLReport"
|
||||
as an explicit template, but the protocol never silently selects it.
|
||||
|
||||
Current shape: `ttEXPORT_SIGNATURES` is the signature witness interface. It
|
||||
carries the full source-validator signature witness and binds it to the matching
|
||||
`ttEXPORT` via `sfTransactionHash`. Network consensus produces it from the
|
||||
accepted sidecar set; standalone/dev helpers may produce the same witness from
|
||||
the local validator key. Ledger build pre-scans the ordered transaction stream
|
||||
into a build-local index, and `ttEXPORT` apply consumes that pre-scanned witness.
|
||||
The index is not an extra consensus input; it is only an efficient lookup over
|
||||
the canonical transaction set. In live consensus builds, `onPreBuild` first
|
||||
removes any pre-existing export witness and re-materializes the witness from the
|
||||
accepted sidecar root. That witness is the signer-membership source for apply;
|
||||
current manifest-cache state must not re-decide which accepted signing keys
|
||||
count. Historical `LedgerReplay` uses the same membership rule because current
|
||||
manifests may no longer map old rotated signing keys. Both paths still verify
|
||||
each signature against the inner transaction and require the parent-view
|
||||
threshold. Direct apply paths that did not run `onPreBuild` remain conservative
|
||||
and filter witness signers through the live active-validator view.
|
||||
Committee deletion is rejected while the owner has any live Export latch. This
|
||||
coarse rule keeps every accepted witness replayable without storing the roster
|
||||
again in every latch or maintaining a metadata-expensive reference count.
|
||||
|
||||
**INV-5 — Store the witness once.**
|
||||
The signature witness is canonical input; metadata is output. Metadata may carry
|
||||
hashes and references for client discovery, but it should not duplicate the full
|
||||
signature payload merely to avoid a client dereference. A convenience RPC may
|
||||
expand `ttEXPORT + witness` into the foreign-chain-submittable blob on read.
|
||||
*Anti-pattern:* storing the same validator signatures once as replay input and
|
||||
again as a full metadata blob without a separate consensus reason.
|
||||
**INV-3 - qC and qV answer different questions.**
|
||||
qC is the content threshold for one account-selected committee:
|
||||
`ceil(0.8 * committeeSize)`. The intent cannot lower it. Committee positions
|
||||
are indexes into the immutable canonical roster.
|
||||
|
||||
**INV-6 — Bounded retry window.**
|
||||
An Export that cannot obtain quorum-aligned signatures within its bounded ledger
|
||||
window retries or expires through normal transaction semantics. It must not wait
|
||||
unboundedly, pick the largest sub-quorum set, or finalize against local trusted
|
||||
configuration as a fallback.
|
||||
qV is the root-alignment threshold over the source consensus active-validator
|
||||
view. It coordinates which complete bounded sidecar union may materialize
|
||||
witnesses. qV does not decide committee membership, prove target authorization,
|
||||
or replace local possession and verification of qC signatures.
|
||||
|
||||
The success-vs-retry decision remains a bounded timing edge, like ordinary
|
||||
consensus convergence: one node may observe the quorum-aligned witness before
|
||||
its deadline while another retries. Validation resolves that ledger disagreement.
|
||||
What must never happen is a "successful" export whose signature bytes come from
|
||||
live collector state, late proposal arrivals, or a node-local sub-quorum set
|
||||
instead of the accepted witness in the transaction stream.
|
||||
Neither denominator is derived from locally observed peers or signatures, and
|
||||
silence never lowers either threshold.
|
||||
|
||||
**INV-7 — Export latches are issuance latches, not global tombstones.**
|
||||
The latch binds the canonical target signing intent and is keyed by `(owner,
|
||||
origin transaction ID W)`, not by one authorization-envelope-dependent target
|
||||
transaction ID or by destination TicketSequence alone. The signed release Memo
|
||||
carries `W`; a fresh Export has a fresh `W`, so erasing one latch cannot re-arm
|
||||
an old stamped XPOP against a later issuance. The obsolete memo-less callback
|
||||
and ticket-keyed latch helper are not supported v1 compatibility paths.
|
||||
**INV-4 - Honest shares are post-validation attestations.**
|
||||
A validator signs only after its node accepts the exact source ledger containing
|
||||
the intent as validated. Closing/building that ledger, emitting the node's own
|
||||
validation, or advancing the build cursor does not unlock signing. Ledger
|
||||
building may run ahead of the validated cursor; Export waits without holding
|
||||
base consensus open.
|
||||
|
||||
The release-stamped target binds source domain, target domain, origin
|
||||
transaction ID `W`, origin ledger sequence, and origin ledger hash in a reserved
|
||||
final `xahau/export` Memo covered by each ordinary target multisignature. Existing
|
||||
user Memos remain byte-identical and in their original order. Malformed,
|
||||
duplicate, misplaced, unsupported, or oversized reserved Memos fail before
|
||||
signing.
|
||||
|
||||
**INV-5 - Live manifests bind committee masters to signing keys.**
|
||||
The immutable roster names validator master identities. Live share production
|
||||
and admission use the manifest cache to map a concrete signing key to the
|
||||
master at its committee position. Missing or ambiguous attribution contributes
|
||||
no share. Manifests do not redefine the stored committee and are never read by
|
||||
accepted witness apply or historical replay.
|
||||
|
||||
Target SignerList compatibility with current validator signing-key AccountIDs
|
||||
is an operator/client responsibility. Source consensus cannot inspect or prove
|
||||
that remote configuration.
|
||||
|
||||
**INV-6 - Align the accepted sidecar root, not the live collector.**
|
||||
Valid shares from the direct post-validation relay and signed proposal carriage
|
||||
join one bounded, commutative collector keyed by `(W, committeePosition)`. A
|
||||
second distinct valid contribution at one position is conflicting and that
|
||||
position contributes nothing to qC.
|
||||
|
||||
The complete bounded admitted-share union is content-addressed and its root is
|
||||
advertised in signed extended positions. Witness materialization uses only an
|
||||
exact qV-aligned root that the local node possesses and independently verifies.
|
||||
Late collector arrivals cannot mutate the accepted root.
|
||||
|
||||
*Anti-pattern:* assembling from the current collector at apply time or treating
|
||||
peer root support as remote payload availability.
|
||||
|
||||
**INV-7 - Ledger-defining signatures enter the transaction stream.**
|
||||
Sidecar memory is deliberation state, not replay input. For each eligible latch
|
||||
that reaches qC under the accepted root, build injects at most one canonical
|
||||
later-ledger `ttEXPORT_SIGNATURES` pseudo transaction. It contains the exact
|
||||
release-stamped target, committee-relative contributor bitmap, and signature
|
||||
entries, bound to `W`.
|
||||
|
||||
Witness apply resolves the immutable committee SLE from the parent state,
|
||||
derives size and qC, validates the contributor positions and every target
|
||||
signature, records the witness transaction ID on the latch, and removes the
|
||||
pending-work link. It does not consult mutable manifest state. Historical
|
||||
replay consumes the same bytes and parent state and never regenerates shares.
|
||||
|
||||
**INV-8 - Store the signature evidence once.**
|
||||
The full release-stamped payload and signatures live in the witness transaction.
|
||||
The latch stores only the witness transaction reference. Read-time tooling may
|
||||
expand that canonical witness into the ordinary foreign-chain transaction, but
|
||||
state and metadata must not duplicate its signature payload merely for client
|
||||
convenience.
|
||||
|
||||
Assembly follows ordinary multisign rules: empty `SigningPubKey`, canonical
|
||||
signer AccountID ordering, no duplicate signer accounts, and at most
|
||||
`STTx::maxMultiSigners()` entries.
|
||||
|
||||
**INV-9 - Export latches name issuance, not one assembled target hash.**
|
||||
The latch key is `(owner, origin transaction ID W)`. Its `sfDigest` commits to
|
||||
the normalized identity-form target, not a signer-subset-dependent final target
|
||||
transaction ID. Any valid qC subset for the exact stamped intent may therefore
|
||||
execute and return without orphaning source state.
|
||||
|
||||
Witness and XPOP are independent monotonic facts. Whichever arrives second
|
||||
symmetrically erases the latch and releases reserve. Cancellation while
|
||||
publication is pending is non-revoking: a lifecycle-control `ttEXPORT` names
|
||||
the exact source issuance `W` in `sfTransactionHash`, unlinks work, and retains
|
||||
callback readiness because already-public signatures cannot be withdrawn. The
|
||||
same bytes retain the latch in every state. `tfExportEraseLatch` is a separate,
|
||||
explicit election that erases that exact latch in any state, releases reserve,
|
||||
and knowingly forfeits a later callback. Publication expiry follows the
|
||||
non-revoking retain path. The undeployed Hook API uses the same W-plus-flags
|
||||
contract. v1 has neither an automatic terminal-retirement clock nor a permanent
|
||||
tombstone graveyard.
|
||||
symmetrically erases the latch and releases reserve. Flagless lifecycle control
|
||||
names exact `W`, unlinks pending work, and retains callback readiness because
|
||||
published signatures cannot be revoked. `tfExportEraseLatch` explicitly erases
|
||||
that latch and knowingly forfeits a later callback. Publication expiry follows
|
||||
the non-revoking retain path. v1 has no permanent tombstones or paid bump.
|
||||
|
||||
Implementation status: origin-keyed creation, W-only lifecycle control,
|
||||
expiry-unlink, and symmetric witness/XPOP recording are present.
|
||||
**INV-10 - Released signatures are public capabilities.**
|
||||
After exact source validation, each admitted share is immediately available on
|
||||
the `export_signatures` subscription and may also ride proposals. Anyone may
|
||||
assemble and submit a target transaction once the destination SignerList's
|
||||
actual threshold is met. The source witness is durable evidence, not a secrecy
|
||||
or submission gate.
|
||||
|
||||
**INV-8 — Export signatures are public capabilities.**
|
||||
Proposal-carried signature shares may be observed, assembled, and submitted as
|
||||
soon as destination quorum exists. Source-side witness agreement governs what
|
||||
Xahau records; it is not a confidentiality or destination-execution gate.
|
||||
Import therefore waits outside consensus when its Export latch does not yet
|
||||
exist and matches a later XPOP against the signature-independent intent.
|
||||
The main defense for exposing shares before source finality is authority
|
||||
equivalence: destination execution must require the same validator-derived
|
||||
authority that Xahau requires to validate and materialize the Export.
|
||||
*Anti-pattern:* relying on proposal timing or canonical signer selection to
|
||||
hide or delay an otherwise valid destination transaction.
|
||||
An operator signer may be required by a destination SignerList for assembly
|
||||
control or defense in depth, but it is not a protocol prerequisite and cannot
|
||||
replace qC. Reserving such an entry reduces the validator committee that fits
|
||||
the target's 32-signer limit.
|
||||
|
||||
**INV-9 — The active source authority must fit the destination protocol.**
|
||||
Export does not publish shares without a ledger-anchored `UNLReport`, and does
|
||||
not publish shares or materialize a result when the source validator population
|
||||
before NegativeUNL filtering exceeds `STTx::maxMultiSigners()`. Silently
|
||||
selecting a capped subset would replace source-view authority with an implicit
|
||||
bridge committee. Any future bounded committee must be an explicit, separately
|
||||
reviewed policy.
|
||||
**INV-11 - The destination trust claim is configuration-specific.**
|
||||
XRPL verifies ordinary keys and signatures, not Xahau finality, committee
|
||||
eligibility, or qV. The account operator must configure its SignerList so the
|
||||
chosen committee identities and weight threshold match the intended custody
|
||||
claim. A lower remote threshold weakens that claim; a higher threshold can
|
||||
strand an otherwise sufficient source witness.
|
||||
|
||||
The current source implementation enforces the 32-signer bound. The bounded MVP
|
||||
deployment additionally requires one destination submitter co-signer, so its
|
||||
practical full-view limit is 31 source validator identities. Source code cannot
|
||||
inspect that remote configuration; activation tooling and monitoring must.
|
||||
The target network's ledger-validation quorum is separate. It validates the
|
||||
containing target ledger, and XPOP proves that target finality on return.
|
||||
|
||||
**INV-10 — Source and destination authorization must be equivalent.**
|
||||
The bounded deployment contract uses the same validator-derived key universe
|
||||
and equivalent weighted threshold for Xahau Export/validation and the target
|
||||
account's SignerList. A lower destination threshold defeats the pre-finality
|
||||
share-exposure defense. A higher threshold is safety-conservative but can leave
|
||||
a successful source latch without enough witness authority to execute.
|
||||
|
||||
The target network's ledger-validation quorum is independent: the SignerList
|
||||
authorizes the account transaction, target consensus validates the containing
|
||||
ledger, and XPOP later proves that finality. Because an ordinary target
|
||||
SignerList is static, configure it against the original pre-NegativeUNL source
|
||||
universe and accept reduced Export liveness during NegativeUNL periods rather
|
||||
than lowering destination authority.
|
||||
|
||||
The bounded MVP requires a submitter co-signer held by the target-submission
|
||||
process. If validator weights total `V`, validator threshold is `q`, submitter
|
||||
weight is `C`, and target quorum is `C + q > V`, validator shares alone cannot
|
||||
execute and the submitter still needs validator weight `q`. It signs only after
|
||||
observing the validated source latch, turning the submitter into a
|
||||
liveness/censorship dependency rather than a sole safety authority.
|
||||
|
||||
A future committee may decouple total UNL size from the destination cap, but it
|
||||
must be explicit ledger-anchored source state with versioned membership and
|
||||
rotation. Full source consensus validates the intent; at most 31 committee keys
|
||||
supply shares; the submitter releases after validation. The target trust claim
|
||||
then becomes committee quorum plus submitter, not full-UNL destination authority.
|
||||
**INV-12 - Work, storage, and waiting are bounded.**
|
||||
Committee size, account latches, global pending work, per-round lanes, relay
|
||||
entries, message bytes, sidecar leaves, publication windows, and witness
|
||||
signatures all have hard caps. Export may use one fixed sidecar alignment
|
||||
window after ordinary transaction-set convergence, but timeout always permits
|
||||
base consensus to continue. Below-qC or unaligned work remains pending until
|
||||
witness, cancellation, explicit erase, or publication expiry.
|
||||
|
||||
## Replay Witness Shape
|
||||
|
||||
The accepted local export sidecar snapshot is not consumed directly by
|
||||
`Export::doApply`.
|
||||
Before ledger build, a producer injects one `ttEXPORT_SIGNATURES` pseudo for
|
||||
each export that has usable signatures. In network mode that producer is the
|
||||
consensus extension accept path; in standalone/dev mode it can be a local helper. The
|
||||
pseudo has no ledger-state effect by itself; it is the ledger's replay witness
|
||||
for the validator signatures.
|
||||
`ttEXPORT_SIGNATURES` is a later-ledger protocol pseudo transaction. It is a
|
||||
self-contained source-history record of the exact target bytes signed and the
|
||||
committee-relative sufficient signature set. The accepted sidecar root selects
|
||||
the material before injection; closed-ledger apply sees only transaction bytes
|
||||
and parent ledger state.
|
||||
|
||||
Metadata stores `sfExportSignatureHash`, a direct reference to the witness
|
||||
pseudo, rather than duplicating the signature payload as an assembled
|
||||
`sfExportedTxn` blob. Clients assemble the final foreign-chain transaction from
|
||||
the original `ttEXPORT` inner transaction plus the witness signatures. Assembly
|
||||
must follow the same deterministic contract as `ExportResultBuilder`: sort
|
||||
signers canonically by AccountID, use an empty `SigningPubKey`, and cap the
|
||||
target-chain `Signers` array at `STTx::maxMultiSigners()` before computing or
|
||||
submitting the blob. The witness may contain extra source-side signatures that
|
||||
are valid replay input but are not part of the target-chain blob. Source-chain
|
||||
export does not prove the destination account's SignerList or quorum policy;
|
||||
that compatibility is an operator/client contract for the chosen target chain.
|
||||
|
||||
This is not an XPOP-style self-contained proof. XPOP embeds its UNL and manifest
|
||||
bundle because it is imported as external proof material. Export witnesses are
|
||||
validated-history replay inputs. If we later want trustless historical
|
||||
re-verification without relying on validated inclusion, the larger design is to
|
||||
ledger-anchor validator signing-key history (for example via `UNLReport`) or to
|
||||
embed manifest proof material; that is intentionally out of scope here.
|
||||
The witness is not an XPOP-style proof that unmodified XRPL can interpret.
|
||||
Validated inclusion is the source-chain attestation. XPOP remains the reverse
|
||||
proof: it demonstrates that one assembled target transaction reached target
|
||||
finality and drives the Hook callback against the matching origin latch.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include <xrpld/app/consensus/ConsensusExtensions.h>
|
||||
#include <xrpld/app/consensus/ExportSignatureHarvester.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/misc/ValidatorKeys.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
|
||||
@@ -252,57 +252,5 @@ signaturesFromWitness(STTx const& witness)
|
||||
return signatures;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
AssembledExportResult
|
||||
assembleImpl(
|
||||
STTx const& innerTx,
|
||||
SignatureSnapshot const& signatures,
|
||||
LedgerIndex currentSeq,
|
||||
uint256 const& exportTxHash,
|
||||
std::optional<uint256> const& exportSignatureHash)
|
||||
{
|
||||
auto multiSigned = buildMultiSignedExportedTxn(innerTx, signatures);
|
||||
auto const signerCount = multiSigned.isFieldPresent(sfSigners)
|
||||
? multiSigned.getFieldArray(sfSigners).size()
|
||||
: 0;
|
||||
auto const signedTxHash = multiSigned.getHash(HashPrefix::transactionID);
|
||||
|
||||
STObject exportResult(sfExportResult);
|
||||
exportResult.setFieldU32(sfLedgerSequence, currentSeq);
|
||||
exportResult.setFieldH256(sfTransactionHash, exportTxHash);
|
||||
if (exportSignatureHash)
|
||||
exportResult.setFieldH256(sfExportSignatureHash, *exportSignatureHash);
|
||||
else
|
||||
exportResult.set(std::move(multiSigned));
|
||||
|
||||
return {std::move(exportResult), signedTxHash, signerCount};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AssembledExportResult
|
||||
assembleDirect(
|
||||
STTx const& innerTx,
|
||||
SignatureSnapshot const& signatures,
|
||||
LedgerIndex currentSeq,
|
||||
uint256 const& exportTxHash)
|
||||
{
|
||||
return assembleImpl(
|
||||
innerTx, signatures, currentSeq, exportTxHash, std::nullopt);
|
||||
}
|
||||
|
||||
AssembledExportResult
|
||||
assembleClosedLedger(
|
||||
STTx const& innerTx,
|
||||
SignatureSnapshot const& signatures,
|
||||
LedgerIndex currentSeq,
|
||||
uint256 const& exportTxHash,
|
||||
uint256 const& exportSignatureHash)
|
||||
{
|
||||
return assembleImpl(
|
||||
innerTx, signatures, currentSeq, exportTxHash, exportSignatureHash);
|
||||
}
|
||||
|
||||
} // namespace ExportResultBuilder
|
||||
} // namespace ripple
|
||||
|
||||
@@ -28,13 +28,6 @@ struct PositionedSignature
|
||||
using PositionedSignatureSnapshot =
|
||||
std::map<std::uint16_t, PositionedSignature>;
|
||||
|
||||
struct AssembledExportResult
|
||||
{
|
||||
STObject metadata;
|
||||
uint256 signedTxHash;
|
||||
std::size_t signerCount = 0;
|
||||
};
|
||||
|
||||
Buffer
|
||||
signExportedTxn(
|
||||
STTx const& innerTx,
|
||||
@@ -60,21 +53,6 @@ buildSignatureWitness(
|
||||
std::optional<PositionedSignatureSnapshot>
|
||||
signaturesFromWitness(STTx const& witness);
|
||||
|
||||
AssembledExportResult
|
||||
assembleDirect(
|
||||
STTx const& innerTx,
|
||||
SignatureSnapshot const& signatures,
|
||||
LedgerIndex currentSeq,
|
||||
uint256 const& exportTxHash);
|
||||
|
||||
AssembledExportResult
|
||||
assembleClosedLedger(
|
||||
STTx const& innerTx,
|
||||
SignatureSnapshot const& signatures,
|
||||
LedgerIndex currentSeq,
|
||||
uint256 const& exportTxHash,
|
||||
uint256 const& exportSignatureHash);
|
||||
|
||||
} // namespace ExportResultBuilder
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ MAGIC_ENUM_FLAG(ripple::PaymentChannelClaimFlags);
|
||||
MAGIC_ENUM_FLAG(ripple::NFTokenMintFlags);
|
||||
MAGIC_ENUM_FLAG(ripple::NFTokenCreateOfferFlags);
|
||||
MAGIC_ENUM_FLAG(ripple::ClaimRewardFlags);
|
||||
MAGIC_ENUM_FLAG(ripple::ExportFlags);
|
||||
MAGIC_ENUM_16(ripple::AccountFlags);
|
||||
|
||||
namespace ripple {
|
||||
@@ -418,6 +419,7 @@ private:
|
||||
addFlagsToJson<MPTokenAuthorizeFlags>(ret, "MPTokenAuthorize");
|
||||
addFlagsToJson<MPTokenIssuanceSetFlags>(ret, "MPTokenIssuanceSet");
|
||||
addFlagsToJson<AMMClawbackFlags>(ret, "AMMClawback");
|
||||
addFlagsToJson<ExportFlags>(ret, "Export");
|
||||
struct FlagData
|
||||
{
|
||||
std::string name;
|
||||
|
||||
Reference in New Issue
Block a user