Add auditsignatures RPC for ECDSA-path regression testing

Walks the active chain in [start_height, end_height] and runs the existing
VerifySignature path on every non-coinbase input. Returns counts plus the
first 100 failures.

Intended use: capture a pre-migration baseline (should be all-zero
failures), then re-run after switching the underlying ECDSA primitive
(e.g. OpenSSL EC -> libsecp256k1) to catch behavioural regressions before
they hit IBD on a peer.

Defaults: start = max(1, tip-1000), end = tip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 18:29:41 -07:00
parent b17a004b83
commit ce27e8e5cf
3 changed files with 101 additions and 0 deletions
+99
View File
@@ -558,6 +558,105 @@ Value recalculatesupply(const Array& params, bool fHelp)
return result;
}
// Walks every non-coinbase transaction input across [start_height, end_height]
// and runs the existing VerifySignature path. Reports counts and the first 100
// failures so the caller can spot regressions when the underlying ECDSA
// implementation changes (e.g. OpenSSL EC -> libsecp256k1).
Value auditsignatures(const Array& params, bool fHelp)
{
if (fHelp || params.size() > 2)
throw runtime_error(
"auditsignatures [start_height] [end_height]\n"
"Walk the active chain in [start_height, end_height] (inclusive) and run\n"
"VerifySignature on every non-coinbase input. Returns counts plus up to 100\n"
"failures.\n"
"Defaults: start = max(1, tip-1000), end = tip.\n"
"Pre-migration this should always report 0 failures; post-migration any non-zero\n"
"result identifies a behavioural regression in the new ECDSA path.");
LOCK(cs_main);
if (!pindexBest)
throw runtime_error("auditsignatures: no best block");
int tip = nBestHeight;
int start = (params.size() > 0) ? params[0].get_int() : std::max(1, tip - 1000);
int end = (params.size() > 1) ? params[1].get_int() : tip;
if (start < 1) throw runtime_error("auditsignatures: start_height must be >= 1");
if (end > tip) throw runtime_error("auditsignatures: end_height exceeds tip");
if (start > end) throw runtime_error("auditsignatures: start_height > end_height");
// Build forward walk by descending from tip.
CBlockIndex* pindex = pindexBest;
while (pindex && pindex->nHeight > end)
pindex = pindex->pprev;
std::vector<CBlockIndex*> walk;
while (pindex && pindex->nHeight >= start) {
walk.push_back(pindex);
pindex = pindex->pprev;
}
std::reverse(walk.begin(), walk.end());
int nBlocksScanned = 0;
int64_t nInputsChecked = 0;
int64_t nInputsFailed = 0;
Array failures;
const size_t kMaxFailures = 100;
auto recordFailure = [&](int height, const uint256& txid, unsigned int vin, const char* reason) {
++nInputsFailed;
if (failures.size() >= kMaxFailures) return;
Object f;
f.push_back(Pair("height", height));
f.push_back(Pair("txid", txid.GetHex()));
f.push_back(Pair("vin", (int)vin));
f.push_back(Pair("reason", reason));
failures.push_back(f);
};
for (CBlockIndex* pi : walk) {
CBlock block;
if (!block.ReadFromDisk(pi, true)) {
++nBlocksScanned;
continue;
}
for (const CTransaction& tx : block.vtx) {
if (tx.IsCoinBase()) continue;
for (unsigned int i = 0; i < tx.vin.size(); ++i) {
const COutPoint& prev = tx.vin[i].prevout;
CTransaction txFrom;
uint256 hashBlock;
if (!GetTransaction(prev.hash, txFrom, hashBlock)) {
recordFailure(pi->nHeight, tx.GetHash(), i, "prevout transaction not found");
continue;
}
if (prev.n >= txFrom.vout.size()) {
recordFailure(pi->nHeight, tx.GetHash(), i, "prevout index out of range");
continue;
}
++nInputsChecked;
if (!VerifySignature(txFrom, tx, i, 0))
recordFailure(pi->nHeight, tx.GetHash(), i, "VerifySignature returned false");
}
}
++nBlocksScanned;
if (nBlocksScanned % 1000 == 0)
printf("auditsignatures: scanned %d blocks, %lld inputs, %lld failures\n",
nBlocksScanned, (long long)nInputsChecked, (long long)nInputsFailed);
}
Object result;
result.push_back(Pair("start_height", start));
result.push_back(Pair("end_height", end));
result.push_back(Pair("blocks_scanned", nBlocksScanned));
result.push_back(Pair("inputs_checked", nInputsChecked));
result.push_back(Pair("inputs_failed", nInputsFailed));
result.push_back(Pair("failures", failures));
return result;
}
// triangles: get information of sync-checkpoint
Value getcheckpoint(const Array& params, bool fHelp)
+1
View File
@@ -322,6 +322,7 @@ static const CRPCCommand vRPCCommands[] =
{ "invalidateblock", &invalidateblock, false, false },
{ "reconsiderblock", &reconsiderblock, false, false },
{ "recalculatesupply", &recalculatesupply, false, false },
{ "auditsignatures", &auditsignatures, true, false },
{ "dumputxoset", &dumputxoset, false, false },
{ "reservebalance", &reservebalance, false, true},
{ "checkwallet", &checkwallet, false, true},
+1
View File
@@ -226,6 +226,7 @@ extern json_spirit::Value getchaintips(const json_spirit::Array& params, bool fH
extern json_spirit::Value invalidateblock(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value reconsiderblock(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value recalculatesupply(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value auditsignatures(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value dumputxoset(const json_spirit::Array& params, bool fHelp);
extern json_spirit::Value getaddressbalance(const json_spirit::Array& params, bool fHelp);