Compare commits

...

4 Commits

Author SHA1 Message Date
tequ
5a2b0785f3 move comment to original 2026-10-07 14:22:04 +09:00
Richard Holland
345b7543e9 Merge branch 'dev' into unlreport-fix 2026-10-07 14:36:51 +10:00
tequ
d9cc66afe9 ci: always run fixed 3-config matrix in nix workflow (#873) 2026-10-07 14:36:37 +10:00
Richard Holland
7c5cceac82 unlreport fix 2026-10-06 17:36:56 +11:00
5 changed files with 186 additions and 167 deletions

View File

@@ -8,12 +8,6 @@ on:
types: [opened, synchronize, reopened, labeled, unlabeled]
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
full_matrix:
description: "Force full matrix (6 configs)"
required: false
default: "false"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -30,37 +24,14 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: escape double quotes
id: escape
shell: bash
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
ESCAPED_PR_TITLE="${PR_TITLE//\"/\\\"}"
echo "title=${ESCAPED_PR_TITLE}" >> "$GITHUB_OUTPUT"
- name: Generate build matrix
id: set-matrix
shell: python
env:
GH_TOKEN: ${{ github.token }}
run: |
import json
import os
import urllib.request
# Full matrix with all 6 compiler configurations
# Each configuration includes all parameters needed by the build job
full_matrix = [
{
"compiler_id": "gcc-11-libstdcxx",
"compiler": "gcc",
"cc": "gcc-11",
"cxx": "g++-11",
"compiler_version": 11,
"stdlib": "default",
"configuration": "Debug",
"job_type": "build"
},
matrix = [
{
"compiler_id": "gcc-13-libstdcxx",
"compiler": "gcc",
@@ -93,17 +64,6 @@ jobs:
"grpc/*": ["-Wno-missing-template-arg-list-after-template-kw"]
}
},
{
"compiler_id": "clang-14-libstdcxx-gcc11",
"compiler": "clang",
"cc": "clang-14",
"cxx": "clang++-14",
"compiler_version": 14,
"stdlib": "libstdcxx",
"clang_gcc_toolchain": 11,
"configuration": "Debug",
"job_type": "build"
},
{
"compiler_id": "clang-16-libstdcxx-gcc13",
"compiler": "clang",
@@ -114,117 +74,8 @@ jobs:
"clang_gcc_toolchain": 13,
"configuration": "Debug",
"job_type": "build"
},
{
"compiler_id": "clang-17-libcxx",
"compiler": "clang",
"cc": "clang-17",
"cxx": "clang++-17",
"compiler_version": 17,
"stdlib": "libcxx",
"configuration": "Debug",
"job_type": "build"
},
{
# Clang 18 - testing if it's faster than Clang 17 with libc++
# Requires patching Conan v1 settings.yml to add version 18
"compiler_id": "clang-18-libcxx",
"compiler": "clang",
"cc": "clang-18",
"cxx": "clang++-18",
"compiler_version": 18,
"stdlib": "libcxx",
"configuration": "Debug",
"job_type": "build"
}
]
# Minimal matrix for PRs and feature branches
minimal_matrix = [
full_matrix[1], # gcc-13 (middle-ground gcc)
full_matrix[2], # clang-20 llvm-cov coverage
full_matrix[3] # clang-14 (mature, stable clang)
]
# Determine which matrix to use based on the target branch
ref = "${{ github.ref }}"
base_ref = "${{ github.base_ref }}" # For PRs, this is the target branch
event_name = "${{ github.event_name }}"
pr_title = """${{ steps.escape.outputs.title }}"""
pr_labels = """${{ join(github.event.pull_request.labels.*.name, ',') }}"""
pr_head_sha = "${{ github.event.pull_request.head.sha }}"
# Get commit message - for PRs, fetch via API since head_commit.message is empty
if event_name == "pull_request" and pr_head_sha:
try:
url = f"https://api.github.com/repos/${{ github.repository }}/commits/{pr_head_sha}"
req = urllib.request.Request(url, headers={
"Accept": "application/vnd.github.v3+json",
"Authorization": f"Bearer {os.environ.get('GH_TOKEN', '')}"
})
with urllib.request.urlopen(req) as response:
data = json.load(response)
commit_message = data["commit"]["message"]
except Exception as e:
print(f"Failed to fetch commit message: {e}")
commit_message = ""
else:
commit_message = """${{ github.event.head_commit.message }}"""
# Debug logging
print(f"Event: {event_name}")
print(f"Ref: {ref}")
print(f"Base ref: {base_ref}")
print(f"PR head SHA: {pr_head_sha}")
print(f"PR title: {pr_title}")
print(f"PR labels: {pr_labels}")
print(f"Commit message: {commit_message}")
# Manual trigger input to force full matrix.
manual_full = "${{ github.event.inputs.full_matrix || 'false' }}" == "true"
# Label/manual overrides, while preserving existing title/commit behavior.
force_full = (
manual_full
or "[ci-nix-full-matrix]" in commit_message
or "[ci-nix-full-matrix]" in pr_title
or ("ci-full-build" in pr_labels and "ci-nix-full-matrix" in pr_labels)
)
force_min = (
"ci-full-build" in pr_labels
)
print(f"Force full matrix: {force_full}")
print(f"Force min matrix: {force_min}")
# Check if this is targeting a main branch
# For PRs: check base_ref (target branch)
# For pushes: check ref (current branch)
main_branches = ["refs/heads/dev", "refs/heads/release", "refs/heads/candidate"]
if force_full:
# Override: always use full matrix if forced by manual input or label.
use_full = True
elif force_min:
# Override: always use minimal matrix if ci-full-build label is present.
use_full = False
elif event_name == "pull_request":
# For PRs, base_ref is just the branch name (e.g., "dev", not "refs/heads/dev")
# Check if the PR targets release or candidate (more critical branches)
use_full = base_ref in ["release", "candidate"]
else:
# For pushes, ref is the full reference (e.g., "refs/heads/dev")
use_full = ref in main_branches
# Select the appropriate matrix
if use_full:
if force_full:
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - forced by [ci-nix-full-matrix] tag")
else:
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - targeting main branch")
matrix = full_matrix
else:
print(f"Using MINIMAL matrix (3 configs) - feature branch/PR")
matrix = minimal_matrix
# Add runs_on based on job_type
for entry in matrix:

View File

@@ -1203,6 +1203,96 @@ class UNLReportVoteNewValidator_test : public beast::unit_test::suite
}
};
/*
* Regression test: UNLReport proposals must not be gated on the N-UNL local
* reliability threshold.
*
* The local node (validator 0) validates exactly `myVals` of the 256 scored
* ledgers, validator 1 validates none (so it is an N-UNL disable candidate and
* absent from the report), every other validator validates all of them.
*
* == myVals <= 128: not active ourselves -> no UNLReport, no UNLModify
* == 129 .. 229: UNLReport proposed, not reliable enough to vote on N-UNL
* == >= 230: UNLReport and one UNLModify (230 used to fall into the
* "Too many!" branch and fail)
*/
class UNLReportVoteLocalReliability_test : public beast::unit_test::suite
{
void
testDoVoting()
{
testcase("Do Voting with local reliability below N-UNL threshold");
struct Case
{
std::uint32_t myVals;
bool expectReport;
bool expectModify;
};
std::array<Case, 6> const cases = {{
{128, false, false},
{129, true, false},
{229, true, false},
{230, true, true},
{231, true, true},
{256, true, true},
}};
for (bool const withVLImport : {true, false})
{
for (auto const& c : cases)
{
std::uint32_t const numNodes = 20;
URNetworkHistory history = {
*this, {numNodes, 0, false, false, withVLImport, {}}};
BEAST_EXPECT(history.goodHistory);
if (!history.goodHistory)
continue;
// scored ledgers are [lastSeq - 256, lastSeq - 1]
auto const lastSeq = history.lastLedger()->seq();
history.walkHistoryAndAddValidations(
[&](std::shared_ptr<Ledger const> const& l,
std::size_t idx) -> bool {
if (idx == 1)
return false;
if (idx != 0)
return true;
return l->seq() + 256 < lastSeq + c.myVals;
});
NegativeUNLVote vote(
history.UNLNodeIDs[0],
history.env.journal,
history.env.app());
auto txSet = std::make_shared<SHAMap>(
SHAMapType::TRANSACTION, history.env.app().getNodeFamily());
vote.doVoting(
history.lastLedger(),
history.UNLKeySet,
history.validations,
txSet);
std::size_t const expectReport =
c.expectReport ? numNodes - 1 + (withVLImport ? 1 : 0) : 0;
std::size_t const expectModify = c.expectModify ? 1 : 0;
std::string const ctx = "myVals=" + std::to_string(c.myVals) +
" withVLImport=" + std::to_string(withVLImport);
BEAST_EXPECTS(countUNLRTx(txSet) == expectReport, ctx);
BEAST_EXPECTS(unl::countTx(txSet) == expectModify, ctx);
}
}
}
void
run() override
{
testDoVoting();
}
};
BEAST_DEFINE_TESTSUITE(UNLReport, ledger, ripple);
BEAST_DEFINE_TESTSUITE(UNLReportNoAmendment, ledger, ripple);
BEAST_DEFINE_TESTSUITE(UNLReportFork, consensus, ripple);
@@ -1215,6 +1305,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(
ripple,
1);
BEAST_DEFINE_TESTSUITE(UNLReportVoteNewValidator, consensus, ripple);
BEAST_DEFINE_TESTSUITE(UNLReportVoteLocalReliability, consensus, ripple);
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////

View File

@@ -348,6 +348,17 @@ RCLConsensus::Adaptor::onClose(
make_shamapitem(tx.first->getTransactionID(), s.slice()));
}
// Keep enough validation history for the next flag ledger's score table
// even when we don't vote this round (not proposing / wrong LCL).
// Otherwise toKeep_ goes stale, validations older than
// validationSET_EXPIRES (10 min, < 256 ledgers) get swept, and we fail
// the next flag's score table too.
if (prevLedger->isVotingLedger())
{
auto const seq = prevLedger->info().seq + 1;
app_.getValidations().setSeqToKeep(seq - 1, seq + FLAG_LEDGER_INTERVAL);
}
// Add pseudo-transactions to the set
if (app_.config().standalone() || (proposing && !wrongLCL))
{

View File

@@ -58,9 +58,19 @@ NegativeUNLVote::doVoting(
unlNodeIDs.emplace(nid);
}
// Build a reliability score table of validators
if (std::optional<hash_map<NodeID, std::uint32_t>> scoreTable =
buildScoreTable(prevLedger, unlNodeIDs, validations))
// Build a reliability score table of validators. This only fails if
// there isn't enough ledger history; it does NOT apply the local
// reliability gate (that is N-UNL specific, see below).
auto const scoreTable =
buildRawScoreTable(prevLedger, unlNodeIDs, validations);
if (!scoreTable)
return;
auto const seq = prevLedger->info().seq + 1;
// N-UNL voting: only vote to disable/re-enable others if we ourselves
// have been reliable over the last flag ledger interval.
if (localNodeReliableForNUNL(*scoreTable, seq))
{
// build next negUnl
auto negUnlKeys = prevLedger->negativeUNL();
@@ -82,7 +92,6 @@ NegativeUNLVote::doVoting(
}
}
auto const seq = prevLedger->info().seq + 1;
purgeNewValidators(seq);
// Process the table and find all candidates to disable or to re-enable
@@ -109,14 +118,33 @@ NegativeUNLVote::doVoting(
"ripple::NegativeUNLVote::doVoting : found node to enable");
addTx(seq, nidToKeyMap.at(n), ToReEnable, initialSet);
}
}
// do reporting when enabled
if (prevLedger->rules().enabled(featureXahauGenesis) &&
scoreTable->size() > 0)
// UNLReport. Previously this sat behind the N-UNL local reliability gate
// (>= 90% of our own validations on-chain in the last 256 ledgers). Every
// ttUNL_REPORT is its own deterministic txn and is voted on individually
// in consensus, so one proposer's skewed view can't change the outcome;
// but gating the whole batch per-proposer made the reports all-or-nothing:
// whenever fewer than ~half the UNL cleared the 90% bar the entire report
// lost the dispute and the flag ledger carried no UNLReport at all.
// Use the same bar the report itself uses for "active".
if (prevLedger->rules().enabled(featureXahauGenesis) &&
!scoreTable->empty())
{
auto const it = scoreTable->find(myId_);
auto const myScore = it == scoreTable->end() ? 0u : it->second;
if (myScore > (FLAG_LEDGER_INTERVAL >> 1))
{
addReportingTx(seq, *scoreTable, nidToKeyMap, initialSet);
addImportVLTx(seq, initialSet);
}
else
{
JLOG(j_.debug())
<< "R-UNL: ledger " << seq << ". Local node only issued "
<< myScore << " validations in last " << FLAG_LEDGER_INTERVAL
<< " ledgers, not proposing UNLReport.";
}
}
}
@@ -277,7 +305,7 @@ NegativeUNLVote::choose(
}
std::optional<hash_map<NodeID, std::uint32_t>>
NegativeUNLVote::buildScoreTable(
NegativeUNLVote::buildRawScoreTable(
std::shared_ptr<Ledger const> const& prevLedger,
hash_set<NodeID> const& unl,
RCLValidations& validations)
@@ -327,6 +355,14 @@ NegativeUNLVote::buildScoreTable(
}
}
return scoreTable;
}
bool
NegativeUNLVote::localNodeReliableForNUNL(
hash_map<NodeID, std::uint32_t> const& scoreTable,
LedgerIndex seq) const
{
// Return false if the validation message history or local node's
// participation in the history is not good.
auto const myValidationCount = [&]() -> std::uint32_t {
@@ -341,23 +377,33 @@ NegativeUNLVote::buildScoreTable(
<< " validations in last " << FLAG_LEDGER_INTERVAL
<< " ledgers."
<< " The reliability measurement could be wrong.";
return {};
return false;
}
else if (
myValidationCount > negativeUNLMinLocalValsToVote &&
myValidationCount <= FLAG_LEDGER_INTERVAL)
{
return scoreTable;
}
else
if (myValidationCount > FLAG_LEDGER_INTERVAL)
{
// cannot happen because validations.getTrustedForLedger does not
// return multiple validations of the same ledger from a validator.
JLOG(j_.error()) << "N-UNL: ledger " << seq << ". Local node issued "
<< myValidationCount << " validations in last "
<< FLAG_LEDGER_INTERVAL << " ledgers. Too many!";
return {};
return false;
}
// NB: the old code used `>` here, so exactly 230 fell through to the
// "Too many!" branch.
return true;
}
std::optional<hash_map<NodeID, std::uint32_t>>
NegativeUNLVote::buildScoreTable(
std::shared_ptr<Ledger const> const& prevLedger,
hash_set<NodeID> const& unl,
RCLValidations& validations)
{
auto scoreTable = buildRawScoreTable(prevLedger, unl, validations);
if (scoreTable &&
!localNodeReliableForNUNL(*scoreTable, prevLedger->info().seq + 1))
return {};
return scoreTable;
}
NegativeUNLVote::Candidates const

View File

@@ -192,6 +192,26 @@ private:
NodeID
choose(uint256 const& randomPadData, std::vector<NodeID> const& candidates);
/**
* Count trusted full validations per UNL validator over the last
* FLAG_LEDGER_INTERVAL ledgers. Fails only on insufficient ledger
* history; does not apply the local reliability gate.
*/
std::optional<hash_map<NodeID, std::uint32_t>>
buildRawScoreTable(
std::shared_ptr<Ledger const> const& prevLedger,
hash_set<NodeID> const& unl,
RCLValidations& validations);
/**
* True if the local node issued enough validations in the score table
* to be trusted to vote on N-UNL changes.
*/
bool
localNodeReliableForNUNL(
hash_map<NodeID, std::uint32_t> const& scoreTable,
LedgerIndex seq) const;
/**
* Build a reliability measurement score table of validators' validation
* messages in the last flag ledger period.