mirror of
https://github.com/Xahau/xahaud.git
synced 2026-07-30 02:20:10 +00:00
fix(consensus): use quorum for export-only sidecars
Export-only originally used unanimity as a conservative substitute for the CE/RNG sidecar machinery. That made sense before Export had its own signed ExtendedPosition field and exportSigSetHash convergence gate. Now Export sidecars are signed and converged independently of RNG, so a quorum-aligned exportSigSetHash plus verified active-view signature quorum is deterministic enough for Export-only mode. Keeping unanimity would let one active validator veto an otherwise converged export round. Update CSF and testnet coverage to treat Export-only the same way: one missing/conflicting signer in a 5-validator network succeeds at 4/5, while below-quorum still retries or expires.
This commit is contained in:
@@ -38,7 +38,7 @@ tests:
|
||||
|
||||
# CE + Export: 1 node suppressed, 4/5 = 80% quorum, should succeed
|
||||
- name: export_ce_one_node_down
|
||||
script: .testnet/scenarios/export/export_unanimity.py
|
||||
script: .testnet/scenarios/export/export_quorum.py
|
||||
params:
|
||||
expect_success: true
|
||||
network:
|
||||
@@ -46,9 +46,9 @@ tests:
|
||||
4:
|
||||
XAHAUD_NO_EXPORT_SIG: "1"
|
||||
|
||||
# --- Export only, no CE (100% quorum, unanimity) ---
|
||||
- name: export_unanimity_all_up
|
||||
script: .testnet/scenarios/export/export_unanimity.py
|
||||
# --- Export only, no CE (80% active-view quorum) ---
|
||||
- name: export_only_all_up
|
||||
script: .testnet/scenarios/export/export_quorum.py
|
||||
params:
|
||||
expect_success: true
|
||||
network:
|
||||
@@ -57,8 +57,21 @@ tests:
|
||||
track_features:
|
||||
- Export
|
||||
|
||||
- name: export_unanimity_node_down
|
||||
script: .testnet/scenarios/export/export_unanimity.py
|
||||
- name: export_only_one_node_down
|
||||
script: .testnet/scenarios/export/export_quorum.py
|
||||
params:
|
||||
expect_success: true
|
||||
network:
|
||||
features:
|
||||
- Export
|
||||
track_features:
|
||||
- Export
|
||||
node_env:
|
||||
4:
|
||||
XAHAUD_NO_EXPORT_SIG: "1"
|
||||
|
||||
- name: export_only_two_nodes_down
|
||||
script: .testnet/scenarios/export/export_quorum.py
|
||||
params:
|
||||
expect_success: false
|
||||
network:
|
||||
@@ -67,5 +80,7 @@ tests:
|
||||
track_features:
|
||||
- Export
|
||||
node_env:
|
||||
3:
|
||||
XAHAUD_NO_EXPORT_SIG: "1"
|
||||
4:
|
||||
XAHAUD_NO_EXPORT_SIG: "1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
""":descr: Test Export without CE (unanimity mode). When all 5 nodes are up,
|
||||
100% quorum is reachable and the export should succeed. When 1 node
|
||||
suppresses sigs (4/5), unanimity fails and the export should expire.
|
||||
""":descr: Test Export quorum behavior. When enough active validators sign,
|
||||
the export should succeed whether or not CE is enabled. When fewer than the
|
||||
active-view quorum sign, the export should expire.
|
||||
|
||||
Parameterized via `expect_success` kwarg from suite.yml.
|
||||
|
||||
@@ -33,7 +33,8 @@ async def scenario(ctx, log, expect_success=True):
|
||||
current_seq = ctx.validated_ledger_index(0)
|
||||
|
||||
log(f"Current ledger: {current_seq}")
|
||||
log(f"Expecting export {'success' if expect_success else 'failure (unanimity)'}")
|
||||
outcome = "success" if expect_success else "failure (below quorum)"
|
||||
log(f"Expecting export {outcome}")
|
||||
|
||||
# --- Submit ttEXPORT ---
|
||||
result = await ctx.submit_and_wait(
|
||||
@@ -77,11 +78,11 @@ async def scenario(ctx, log, expect_success=True):
|
||||
# Assert shadow ticket was created
|
||||
assert_shadow_ticket(ctx, alice.address, log, expect_exists=True)
|
||||
|
||||
log("Export succeeded as expected (all validators signed)")
|
||||
log("Export succeeded as expected (active-view quorum reached)")
|
||||
else:
|
||||
if engine_result == "tesSUCCESS":
|
||||
raise AssertionError(
|
||||
"Export should NOT have succeeded with sub-unanimity"
|
||||
"Export should NOT have succeeded below active-view quorum"
|
||||
)
|
||||
log(f"Export failed as expected ({engine_result})")
|
||||
|
||||
@@ -976,12 +976,12 @@ public:
|
||||
}
|
||||
|
||||
void
|
||||
testExportOnlyRequiresUnanimousAlignment()
|
||||
testExportOnlyQuorumIgnoresMinorityConflict()
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
|
||||
testcase("Export-only sig set requires unanimous alignment");
|
||||
testcase("Export-only sig set quorum ignores minority conflict");
|
||||
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
@@ -998,12 +998,17 @@ public:
|
||||
|
||||
sim.run(3);
|
||||
|
||||
BEAST_EXPECT(sim.branches(peers) == 1);
|
||||
for (Peer const* peer : peers)
|
||||
PeerGroup honest{
|
||||
std::vector<Peer*>{peers[1], peers[2], peers[3], peers[4]}};
|
||||
BEAST_EXPECT(sim.branches(honest) == 1);
|
||||
BEAST_EXPECT(sim.synchronized(honest));
|
||||
|
||||
for (Peer const* peer : honest)
|
||||
{
|
||||
BEAST_EXPECT(!peer->ce().lastExportSucceeded_);
|
||||
BEAST_EXPECT(peer->ce().lastExportRetried_);
|
||||
BEAST_EXPECT(peer->ce().lastExportSucceeded_);
|
||||
BEAST_EXPECT(!peer->ce().lastExportRetried_);
|
||||
}
|
||||
BEAST_EXPECT(!peers[0]->ce().lastExportSucceeded_);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1103,7 +1108,7 @@ public:
|
||||
} while (false)
|
||||
|
||||
RUN(testExportOnlySteadyStateSucceeds);
|
||||
RUN(testExportOnlyRequiresUnanimousAlignment);
|
||||
RUN(testExportOnlyQuorumIgnoresMinorityConflict);
|
||||
RUN(testExportSigSetQuorumAlignmentIgnoresMinorityConflict);
|
||||
RUN(testExportSigSetConflictWithoutQuorumRetries);
|
||||
|
||||
|
||||
@@ -398,7 +398,7 @@ struct Peer
|
||||
return (std::numeric_limits<std::size_t>::max)() / 4;
|
||||
auto const base =
|
||||
unlNodes_.empty() ? std::size_t{1} : unlNodes_.size();
|
||||
return enableRngConsensus_ ? calculateQuorumThreshold(base) : base;
|
||||
return calculateQuorumThreshold(base);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
|
||||
@@ -235,10 +235,11 @@ ConsensusExtensions::exportSigQuorumThreshold() const
|
||||
if (base == 0)
|
||||
return 1;
|
||||
|
||||
// Export can operate without ConsensusEntropy. In that mode it uses the
|
||||
// original unanimity rule, but still relies on the same sidecar alignment
|
||||
// gate so all nodes make the same accept-time decision.
|
||||
return rngEnabled() ? calculateQuorumThreshold(base) : base;
|
||||
// Export sidecar hashes are signed through ExtendedPosition even when RNG
|
||||
// is disabled, so a quorum-aligned exportSigSetHash is deterministic
|
||||
// enough for Export-only mode. Unanimity would let one active validator
|
||||
// veto an otherwise converged export round.
|
||||
return calculateQuorumThreshold(base);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -156,11 +156,12 @@ conflicts and produce asymmetric zero/non-zero outcomes.
|
||||
`featureExport` and `featureConsensusEntropy` are independently amendment
|
||||
gated.
|
||||
|
||||
Export can run without ConsensusEntropy, but then it uses a conservative
|
||||
ephemeral mode: verified export signature sidecars still converge through
|
||||
`ExtendedPosition`, but success requires unanimity of the active validator
|
||||
view. With ConsensusEntropy active, the same sidecar machinery uses the normal
|
||||
80% threshold.
|
||||
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.
|
||||
|
||||
The extended proposal machinery is enabled when either feature needs signed
|
||||
sidecar fields. Do not make Export depend on RNG availability just because RNG
|
||||
@@ -180,9 +181,11 @@ once a candidate tx set exists, only signatures verified against the `ttEXPORT`
|
||||
in that candidate set may become quorum material or enter `exportSigSetHash`.
|
||||
|
||||
Export success requires quorum alignment on `exportSigSetHash`, not merely a
|
||||
local collector quorum. If the verified signature set cannot align by the
|
||||
bounded deadline, the export retries or expires according to normal transaction
|
||||
rules.
|
||||
local collector quorum. If a quorum of tx-converged participants advertises the
|
||||
same export signature sidecar hash, that hash is aligned and below-quorum
|
||||
conflicts are ignored. 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 must not promote unverified proposal-carried signatures into
|
||||
current-round quorum material. It may verify and retain them for a future retry,
|
||||
@@ -198,8 +201,8 @@ Accept-time cleanup must preserve Export state through `buildLCL` whenever
|
||||
applies.
|
||||
|
||||
CSF consensus tests model the export sidecar gate directly. Testnet scenarios
|
||||
under `.testnet/scenarios/export/` cover live-node Export+CE behavior and the
|
||||
Export-only unanimity mode.
|
||||
under `.testnet/scenarios/export/` cover live-node Export+CE behavior and
|
||||
Export-only quorum behavior.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
@@ -218,4 +221,6 @@ When changing consensus extension code, check these questions:
|
||||
- Are export signatures verified before they count?
|
||||
- Does export success require `exportSigSetHash` alignment, not just local
|
||||
collector quorum?
|
||||
- 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.
|
||||
- Are CE and Export still independently gated and independently stoppable?
|
||||
|
||||
@@ -132,9 +132,9 @@ Export::doApply()
|
||||
// check and sign directly with our validator keys in the blob
|
||||
// assembly step below.
|
||||
//
|
||||
// Network mode:
|
||||
// With CE: 80% quorum (SHAMap convergence ensures agreement).
|
||||
// Without CE: unanimity (avoids non-deterministic disagreement).
|
||||
// Network mode: active-view 80% quorum. Export-only rounds are still
|
||||
// deterministic because exportSigSetHash is signed in ExtendedPosition and
|
||||
// converged before closed-ledger apply can use the signatures.
|
||||
// Deserialize the inner tx early — needed both for the upgrade
|
||||
// pass (verify unverified sigs) and for blob assembly.
|
||||
auto const& exportedObj =
|
||||
@@ -187,14 +187,8 @@ Export::doApply()
|
||||
|
||||
if (!ctx_.app.config().standalone())
|
||||
{
|
||||
std::size_t threshold;
|
||||
bool const ceEnabled = view().rules().enabled(featureConsensusEntropy);
|
||||
if (unlSize == 0)
|
||||
threshold = 1;
|
||||
else if (ceEnabled)
|
||||
threshold = calculateQuorumThreshold(unlSize);
|
||||
else
|
||||
threshold = unlSize;
|
||||
std::size_t const threshold =
|
||||
unlSize == 0 ? 1 : calculateQuorumThreshold(unlSize);
|
||||
|
||||
// The collector may contain old trusted signatures; quorum counts only
|
||||
// signatures whose keys resolve into the same frozen active view.
|
||||
|
||||
Reference in New Issue
Block a user