fix(export): require anchored view for finalization

This commit is contained in:
Nicholas Dudfield
2026-07-16 23:09:16 +07:00
parent 9dcaebd8e5
commit 4e313098a6
4 changed files with 67 additions and 1 deletions

View File

@@ -395,6 +395,7 @@ struct FakeExtensions
bool localExportSigs{true};
bool livePendingExportLatches{false};
bool exportOn{true};
bool exportViewAnchored{true};
bool entropyFailed{false};
bool commitFrozen{false};
std::size_t sidecarQuorum{4};
@@ -412,6 +413,7 @@ struct FakeExtensions
std::deque<uint256> entropyHashSequence;
int commitBuilds = 0;
int exportBuilds = 0;
int acceptedExportClears = 0;
int entropyBuilds = 0;
int participantDiagnostics = 0;
int selfSeeds = 0;
@@ -428,6 +430,12 @@ struct FakeExtensions
return exportOn;
}
bool
exportFinalizationViewAnchored() const
{
return exportViewAnchored;
}
bool
testSuppressExportSigSetHash() const
{
@@ -611,6 +619,7 @@ struct FakeExtensions
void
clearAcceptedExportSigSet()
{
++acceptedExportClears;
}
template <class PeerPositions>
@@ -4096,6 +4105,25 @@ class ConsensusExtensions_test : public beast::unit_test::suite
BEAST_EXPECT(ext.exportBuilds == 0);
}
void
testExportSigGateSkipsWithoutAnchoredView()
{
testcase("Export sig gate skips without ledger-anchored view");
FakeExtensions ext;
ext.exportViewAnchored = false;
ExtensionTickHarness harness;
harness.addPeer(1, ext.exportHash);
auto const result = harness.tick(ext);
BEAST_EXPECT(result.readyForAccept);
BEAST_EXPECT(!ext.exportSigGateStarted_);
BEAST_EXPECT(!harness.position.exportSigSetHash);
BEAST_EXPECT(ext.exportBuilds == 0);
BEAST_EXPECT(ext.acceptedExportClears == 1);
}
void
testParticipantDiagnosticsOnlyWhenExtensionEnabled()
{
@@ -4886,6 +4914,7 @@ public:
testExportSigGateRefreshesHashBeforeWaiting();
testExportSigGateBoundsCandidateObservationWindow();
testExportSigGateSkipsWhenExportDisabled();
testExportSigGateSkipsWithoutAnchoredView();
testParticipantDiagnosticsOnlyWhenExtensionEnabled();
testExportDisabledRoundClearsCollector();
testValidatorKeylessAuthoringNoops();

View File

@@ -1713,6 +1713,12 @@ ConsensusExtensions::exportEnabled() const
return exportEnabledThisRound_.load(std::memory_order_relaxed);
}
bool
ConsensusExtensions::exportFinalizationViewAnchored() const
{
return app_.config().standalone() || activeValidatorView()->fromUNLReport;
}
bool
ConsensusExtensions::testSuppressExportSigSetHash() const
{
@@ -2544,7 +2550,7 @@ ConsensusExtensions::onPreBuild(
//@@end rng-inject-pseudotx
}
if (exportEnabled())
if (exportEnabled() && exportFinalizationViewAnchored())
{
//@@start export-later-ledger-witness-materialization
// Standalone has no peer-position gate to build and accept a sidecar
@@ -2656,6 +2662,16 @@ ConsensusExtensions::onPreBuild(
}
//@@end export-later-ledger-witness-materialization
}
else if (exportEnabled())
{
// Admission currently makes this unreachable for a live latch because
// UNLReport state persists in descendants. Retain the materialization
// guard so a future report-expiry or clearing rule cannot silently
// turn node-local configured trust into ledger-defining authority.
JLOG(j_.warn()) << "Export: skipping witness materialization"
<< " reason=no-unl-report"
<< " buildSeq=" << seq;
}
//@@start accept-time-cleanup-success
// Export's ledger-defining signature witnesses are now self-contained

View File

@@ -388,6 +388,12 @@ public:
bool
exportEnabled() const;
/// Whether Export may use the current round's validator view to align and
/// materialize a witness. Standalone is deterministic locally; networked
/// operation requires the view to come from the parent UNLReport.
bool
exportFinalizationViewAnchored() const;
bool
testSuppressExportSigSetHash() const;

View File

@@ -939,6 +939,21 @@ extensionsTick(Ext& ext, Ctx const& ctx)
if (!ext.exportEnabled())
return {.readyForAccept = true};
// Export admission already requires a nonempty parent UNLReport, and
// current ledger rules never erase or empty that report afterward, so
// this should be unreachable while an admitted latch is live. Keep the
// finalization boundary fail-closed in case report expiry or clearing
// semantics are introduced later: a config-derived view is node-local
// and cannot safely align a ledger-defining Export witness.
if (!ext.exportFinalizationViewAnchored())
{
ext.clearAcceptedExportSigSet();
JLOG(ext.j_.warn()) << "Export: skipping signature-set alignment"
<< " reason=no-unl-report"
<< " buildSeq=" << ctx.buildSeq;
return {.readyForAccept = true};
}
auto startExportSigGate = [&]() -> bool {
if (ext.exportSigGateStarted_)
return false;