Skip to content

Commit 3f9e778

Browse files
jamescowensclaude
andcommitted
consensus: fix timing overhead, race condition, and unnecessary computation (gridcoin-community#2880)
- Replace MilliTimer with lightweight GetTimeMillis() deltas in GridcoinConnectBlock, guarded by IsShadowValidationEnabled() so no timing overhead is incurred on the hot validation path when shadow mode is off - Fix race between LogShadowResult and ShutdownShadowValidator by moving is_open() check inside the mutex and guarding close() with the same mutex - Move mandatory/local sidestake spec computation inside the fEnableSideStaking guard so stake-splitting-only runs skip the registry queries and spec computation entirely Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0bd25cd commit 3f9e778

File tree

3 files changed

+80
-70
lines changed

3 files changed

+80
-70
lines changed

src/gridcoin/consensus/shadow_validator.cpp

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,30 +70,32 @@ void LogShadowResult(
7070
}
7171

7272
// Write to structured log file if open.
73-
if (g_shadow_log_file.is_open()) {
73+
{
7474
std::lock_guard<std::mutex> lock(g_shadow_log_mutex);
7575

76-
g_shadow_log_file
77-
<< "{\"height\":" << height
78-
<< ",\"block_hash\":\"" << block_hash.ToString() << "\""
79-
<< ",\"component\":\"" << component << "\""
80-
<< ",\"auth_impl\":\"" << g_consensus_impl << "\""
81-
<< ",\"auth_pass\":" << (auth_pass ? "true" : "false")
82-
<< ",\"shadow_pass\":" << (shadow_pass ? "true" : "false");
83-
84-
if (!auth_error.empty()) {
85-
g_shadow_log_file << ",\"auth_error\":\"" << JsonEscape(auth_error) << "\"";
86-
}
87-
if (!shadow_error.empty()) {
88-
g_shadow_log_file << ",\"shadow_error\":\"" << JsonEscape(shadow_error) << "\"";
89-
}
76+
if (g_shadow_log_file.is_open()) {
77+
g_shadow_log_file
78+
<< "{\"height\":" << height
79+
<< ",\"block_hash\":\"" << block_hash.ToString() << "\""
80+
<< ",\"component\":\"" << component << "\""
81+
<< ",\"auth_impl\":\"" << g_consensus_impl << "\""
82+
<< ",\"auth_pass\":" << (auth_pass ? "true" : "false")
83+
<< ",\"shadow_pass\":" << (shadow_pass ? "true" : "false");
84+
85+
if (!auth_error.empty()) {
86+
g_shadow_log_file << ",\"auth_error\":\"" << JsonEscape(auth_error) << "\"";
87+
}
88+
if (!shadow_error.empty()) {
89+
g_shadow_log_file << ",\"shadow_error\":\"" << JsonEscape(shadow_error) << "\"";
90+
}
9091

91-
g_shadow_log_file
92-
<< ",\"auth_ms\":" << auth_ms
93-
<< ",\"shadow_ms\":" << shadow_ms
94-
<< "}\n";
92+
g_shadow_log_file
93+
<< ",\"auth_ms\":" << auth_ms
94+
<< ",\"shadow_ms\":" << shadow_ms
95+
<< "}\n";
9596

96-
g_shadow_log_file.flush();
97+
g_shadow_log_file.flush();
98+
}
9799
}
98100
}
99101

@@ -168,6 +170,8 @@ void GRC::LogShadowComparison(
168170

169171
void GRC::ShutdownShadowValidator()
170172
{
173+
std::lock_guard<std::mutex> lock(g_shadow_log_mutex);
174+
171175
if (g_shadow_log_file.is_open()) {
172176
g_shadow_log_file.close();
173177
LogPrintf("ShadowValidator: closed shadow log file");

src/miner.cpp

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -906,50 +906,53 @@ void SplitCoinStakeOutput(CBlock &blocknew, int64_t &nReward, bool &fEnableStake
906906
// Initialize nOutputUsed at 1, because one is already used for the empty coinstake flag output.
907907
unsigned int nOutputsUsed = 1;
908908

909-
// Compute the mandatory sidestake spec using BlockRewardRules — the shared invariant computation that
910-
// both miner and validator consume. This replaces the separate ActiveSideStakeEntries() call and manual
911-
// dust/address filtering that previously lived only here, eliminating the class of drift bugs where the
912-
// miner and validator made different eligibility decisions (see #2848).
913-
CTxDestination coinstake_dest;
914-
bool have_coinstake_dest = ExtractDestination(CoinStakeScriptPubKey, coinstake_dest);
915-
if (!have_coinstake_dest) {
916-
LogPrintf("WARN: SplitCoinStakeOutput: could not extract coinstake destination, "
917-
"skipping mandatory sidestake spec computation.");
918-
}
919-
920-
GRC::BlockRewardRules rules(pindexBest, blocknew.nVersion, blocknew.nTime);
921-
909+
// Mandatory and local sidestake computation — only needed when sidestaking is active.
922910
std::vector<GRC::BlockRewardRules::MandatorySidestakeSpec> mandatory_specs;
911+
SideStakeAlloc local_sidestakes;
912+
unsigned int nMandatoryOutputLimit = GetMandatorySideStakeOutputLimit(blocknew.nVersion);
923913

924-
if (have_coinstake_dest) {
925-
mandatory_specs = GRC::BlockRewardRules::FilterEligible(
926-
rules.ComputeEligibleMandatorySidestakes(coinstake_dest, nReward));
927-
}
914+
if (fEnableSideStaking) {
915+
// Compute the mandatory sidestake spec using BlockRewardRules — the shared invariant computation that
916+
// both miner and validator consume. This replaces the separate ActiveSideStakeEntries() call and manual
917+
// dust/address filtering that previously lived only here, eliminating the class of drift bugs where the
918+
// miner and validator made different eligibility decisions (see #2848).
919+
CTxDestination coinstake_dest;
920+
bool have_coinstake_dest = ExtractDestination(CoinStakeScriptPubKey, coinstake_dest);
921+
if (!have_coinstake_dest) {
922+
LogPrintf("WARN: SplitCoinStakeOutput: could not extract coinstake destination, "
923+
"skipping mandatory sidestake spec computation.");
924+
}
928925

929-
unsigned int nMandatoryOutputLimit = GetMandatorySideStakeOutputLimit(blocknew.nVersion);
926+
if (have_coinstake_dest) {
927+
GRC::BlockRewardRules rules(pindexBest, blocknew.nVersion, blocknew.nTime);
930928

931-
// Shuffle when over the limit — non-deterministic selection. The validator cannot reproduce the shuffle
932-
// and doesn't need to; it matches against the eligible set regardless of order.
933-
if (mandatory_specs.size() > nMandatoryOutputLimit) {
934-
Shuffle(mandatory_specs.begin(), mandatory_specs.end(), FastRandomContext());
935-
}
929+
mandatory_specs = GRC::BlockRewardRules::FilterEligible(
930+
rules.ComputeEligibleMandatorySidestakes(coinstake_dest, nReward));
931+
}
936932

937-
// If the number of voluntary sidestaking allocation entries exceeds the remaining output capacity, then
938-
// shuffle the list to support sidestaking with more than the available number of entries. This is a super
939-
// simple solution but has some disadvantages. If the person made a mistake and has the entries in the config
940-
// file add up to more than 100%, then those entries resulting in a cumulative total over 100% will always be
941-
// excluded, not just randomly excluded, because the cumulative check is done in the order of the entries in
942-
// the config file. This is not regarded as a big issue, because all of the entries are supposed to add up to
943-
// less than or equal to 100%. Also when there are more than the available number of entries, the residual
944-
// returned to the coinstake will vary when the entries are shuffled, because the total percentage of the
945-
// selected entries will be randomized. No attempt to renormalize the percentages is done.
946-
SideStakeAlloc local_sidestakes
947-
= GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::LOCAL, false);
948-
949-
if (local_sidestakes.size() > nMaxSideStakeOutputs
950-
- std::min<unsigned int>(nMandatoryOutputLimit,
951-
mandatory_specs.size())) {
952-
Shuffle(local_sidestakes.begin(), local_sidestakes.end(), FastRandomContext());
933+
// Shuffle when over the limit — non-deterministic selection. The validator cannot reproduce the shuffle
934+
// and doesn't need to; it matches against the eligible set regardless of order.
935+
if (mandatory_specs.size() > nMandatoryOutputLimit) {
936+
Shuffle(mandatory_specs.begin(), mandatory_specs.end(), FastRandomContext());
937+
}
938+
939+
// If the number of voluntary sidestaking allocation entries exceeds the remaining output capacity, then
940+
// shuffle the list to support sidestaking with more than the available number of entries. This is a super
941+
// simple solution but has some disadvantages. If the person made a mistake and has the entries in the config
942+
// file add up to more than 100%, then those entries resulting in a cumulative total over 100% will always be
943+
// excluded, not just randomly excluded, because the cumulative check is done in the order of the entries in
944+
// the config file. This is not regarded as a big issue, because all of the entries are supposed to add up to
945+
// less than or equal to 100%. Also when there are more than the available number of entries, the residual
946+
// returned to the coinstake will vary when the entries are shuffled, because the total percentage of the
947+
// selected entries will be randomized. No attempt to renormalize the percentages is done.
948+
local_sidestakes
949+
= GRC::GetSideStakeRegistry().ActiveSideStakeEntries(GRC::SideStake::FilterFlag::LOCAL, false);
950+
951+
if (local_sidestakes.size() > nMaxSideStakeOutputs
952+
- std::min<unsigned int>(nMandatoryOutputLimit,
953+
mandatory_specs.size())) {
954+
Shuffle(local_sidestakes.begin(), local_sidestakes.end(), FastRandomContext());
955+
}
953956
}
954957

955958
// Initialize remaining stake output value to the total value of output for stake, which also includes

src/validation.cpp

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1561,46 +1561,49 @@ bool GridcoinConnectBlock(
15611561
std::string shadow_error;
15621562
int64_t shadow_ms = 0;
15631563

1564+
const bool shadow_enabled = GRC::IsShadowValidationEnabled();
1565+
15641566
if (GRC::IsNewImplAuthoritative()) {
15651567
// BlockRewardRules is authoritative.
1566-
MilliTimer auth_timer;
1568+
int64_t t_start = shadow_enabled ? GetTimeMillis() : 0;
15671569

15681570
GRC::BlockRewardRules rules(block, pindex, stake_value_in, total_claimed, fees, out_coin_age);
15691571
auth_pass = rules.Check(auth_error);
1570-
auth_ms = auth_timer.GetTimes("auth_check").elapsed_time;
1572+
1573+
if (shadow_enabled) auth_ms = GetTimeMillis() - t_start;
15711574

15721575
if (!auth_pass) {
15731576
block.DoS(10, error("%s: BlockRewardRules::Check FAILED at height %d: %s",
15741577
__func__, pindex->nHeight, auth_error));
15751578
}
15761579

15771580
// Shadow: run old ClaimValidator for comparison.
1578-
if (GRC::IsShadowValidationEnabled()) {
1579-
MilliTimer shadow_timer;
1581+
if (shadow_enabled) {
1582+
int64_t t_shadow = GetTimeMillis();
15801583
shadow_pass = ClaimValidator(block, pindex, stake_value_in, total_claimed, fees, out_coin_age).Check();
1581-
shadow_ms = shadow_timer.GetTimes("shadow_check").elapsed_time;
1584+
shadow_ms = GetTimeMillis() - t_shadow;
15821585

15831586
if (!shadow_pass) {
15841587
shadow_error = "ClaimValidator::Check() failed (see debug log for details)";
15851588
}
15861589
}
15871590
} else {
15881591
// ClaimValidator is authoritative (default).
1589-
MilliTimer auth_timer;
1592+
int64_t t_start = shadow_enabled ? GetTimeMillis() : 0;
15901593
auth_pass = ClaimValidator(block, pindex, stake_value_in, total_claimed, fees, out_coin_age).Check();
1591-
auth_ms = auth_timer.GetTimes("auth_check").elapsed_time;
1594+
if (shadow_enabled) auth_ms = GetTimeMillis() - t_start;
15921595

15931596
// Shadow: run BlockRewardRules for comparison.
1594-
if (GRC::IsShadowValidationEnabled()) {
1595-
MilliTimer shadow_timer;
1597+
if (shadow_enabled) {
1598+
int64_t t_shadow = GetTimeMillis();
15961599

15971600
GRC::BlockRewardRules rules(block, pindex, stake_value_in, total_claimed, fees, out_coin_age);
15981601
shadow_pass = rules.Check(shadow_error);
1599-
shadow_ms = shadow_timer.GetTimes("shadow_check").elapsed_time;
1602+
shadow_ms = GetTimeMillis() - t_shadow;
16001603
}
16011604
}
16021605

1603-
if (GRC::IsShadowValidationEnabled()) {
1606+
if (shadow_enabled) {
16041607
GRC::LogShadowComparison(pindex->nHeight, pindex->GetBlockHash(),
16051608
auth_pass, shadow_pass,
16061609
auth_error, shadow_error,

0 commit comments

Comments
 (0)