Skip to content

feat(checksig-census): capture cumulative script evidence - #54

Merged
metaphorics merged 1 commit into
mainfrom
atomic/checksig-census-cumulative-evidence
Aug 18, 2026
Merged

feat(checksig-census): capture cumulative script evidence#54
metaphorics merged 1 commit into
mainfrom
atomic/checksig-census-cumulative-evidence

Conversation

@metaphorics

@metaphorics metaphorics commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • capture cumulative context, signature-record, and journal evidence from the patched kernel
  • keep capture and bare-secp readers aligned with the binary evidence contracts
  • document the exact census workflow and integrity checks

Verification

  • capture utility format, clippy, and tests
  • bare-secp release clippy and 12 release tests
  • repository CI clippy and workspace tests on the complete stack

Part of #42

Closes #58

Emit canonical context, journal, and operation streams with fixed cumulative counters and fail-closed native sink handling.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added configurable block-range extraction and sampling limits.
    • Added verification reports with extraction metadata and expanded timing support.
    • Added detailed signature-verification census data, including Schnorr outcomes, rejection reasons, and per-input context.
    • Added corpus hashing, height filtering, and JSONL context output for analysis workflows.
    • Added benchmark support for bare signature verification.
  • Documentation

    • Expanded census format documentation, validation requirements, and rejection semantics.

Walkthrough

The change adds 24-counter census instrumentation, streamed record, journal, and context outputs, filtered corpus capture with JSONL sidecars, and configurable kernel verification extraction with schema v2 reports.

Changes

Census verification pipeline

Layer / File(s) Summary
Census ABI and streamed artifacts
tools/checksig-census/instrumentation.diff
The census ABI now exposes reset, snapshot, flush, counter-count, and bare-verification benchmark functions. Record, journal, and context data use synchronized streamed sinks with headers, counts, flushing, reset, and error handling.
Script verification instrumentation
tools/checksig-census/instrumentation.diff
Script verification records CHECKSIG, CHECKSIGVERIFY, CHECKSIGADD, ECDSA, Schnorr, sighash, rejection, and verification-call events.
Filtered corpus capture and sidecars
tools/checksig-census/capture/src/main.rs
The capture harness filters blocks by height, applies block-hash-aware production flags, configures census outputs, writes per-input JSONL context rows, and records corpus and artifact hashes.
Configurable kernel verification extraction
crates/consensus/examples/kernel_verify_spike.rs
The spike requires corpus and output paths, validates extraction bounds and thresholds, samples the configured height range, handles the mainnet BIP16 exception, and writes schema v2 reports.
Counter contract and replay validation
tools/checksig-census/bare-secp/src/main.rs, tools/checksig-census/README.md
The bare-secp tool uses 24 counters. The census workflow documents C150 replay validation, context requirements, rejection reasons, and updated binary formats.

Sequence Diagram(s)

sequenceDiagram
  participant CaptureHarness
  participant BitcoinKernel
  participant CensusState
  participant CensusArtifacts
  participant ContextSidecar

  CaptureHarness->>BitcoinKernel: verify selected block transactions
  BitcoinKernel->>CensusState: record operation and verification outcome
  CensusState->>CensusArtifacts: stream counters, records, and journals
  BitcoinKernel->>ContextSidecar: write transaction and input context
  CaptureHarness->>CensusArtifacts: flush census outputs
  CaptureHarness->>CaptureHarness: write summary and artifact hashes
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.23% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title follows Conventional Commits style and clearly describes the cumulative script evidence capture changes.
Description check ✅ Passed The description directly summarizes the evidence capture, binary contract alignment, documentation, and verification work.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch atomic/checksig-census-cumulative-evidence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tools/checksig-census/instrumentation.diff (2)

891-943: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Narrowing size() to uint8_t corrupts records and can abort the downstream tool.

(uint8_t)vchSig.size() and (uint8_t)vchPubKey.size() truncate silently. Script stack elements go up to 520 bytes, and the C150 range 0..150000 is pre-BIP66, so non-DER oversized pushes do reach CheckECDSASignature. Two failure modes follow:

  • A 300-byte pubkey becomes pubkey_len == 44. The record looks valid and is wrong. tools/checksig-census/bare-secp/src/main.rs accepts it and feeds 44 bytes of a 65-byte truncated buffer to the benchmark.
  • A 100-byte signature stays 100, which exceeds 72. parse_record in tools/checksig-census/bare-secp/src/main.rs (Line 359) then aborts the entire run on a record the kernel itself wrote.

Clamp explicitly at the emission site and mark the truncation so the analyzer can see it, instead of pretending the length fit.

🐛 Sketch: clamp instead of narrowing
++// Clamp oversized script elements; the record buffers hold 72/65 bytes.
++static inline uint8_t census_clamp_len(size_t n, size_t cap) {
++    return static_cast<uint8_t>(n > cap ? cap : n);
++}

Then replace each (uint8_t)vchSig.size() with census_clamp_len(vchSig.size(), 72) and each (uint8_t)vchPubKey.size() with census_clamp_len(vchPubKey.size(), 65). Document the clamp in the README records section so a clamped length is not read as an exact length.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/checksig-census/instrumentation.diff` around lines 891 - 943, Replace
every direct uint8_t cast of vchSig.size() and vchPubKey.size() in the
census_emit_record calls within CheckECDSASignature with explicit
census_clamp_len limits of 72 and 65 respectively, and propagate a truncation
indicator in the record format so analyzers can distinguish clamped lengths from
exact lengths. Update the README records documentation to describe the clamping
semantics.

151-215: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not let C++ exceptions escape this extern "C" function.

count is a caller-supplied uint64_t. reserve(count) throws std::length_error or std::bad_alloc for large or absurd values, and emplace_back can throw too. This function has no try/catch. The rest of bitcoinkernel.cpp wraps exported bodies in try/catch for exactly this reason. An exception unwinding across the C ABI into Rust is undefined behavior, not a clean error.

Bound count and catch everything.

🐛 Proposed fix: fail closed instead of unwinding
 +int btck_bare_verify_bench(const btck_bare_input* inputs, uint64_t count,
 +                            uint32_t warmup_rounds, uint32_t timed_rounds,
 +                            uint32_t mode, btck_bare_result* result)
 +{
 +    if (mode != 0 || inputs == nullptr || result == nullptr || count == 0) return -1;
 +    if (timed_rounds == 0 || timed_rounds > 64) return -1;
++    if (count > (std::numeric_limits<size_t>::max() / sizeof(CPubKey))) return -1;
++    try {
 +    // Prebuild CPubKey, DER vectors, and uint256 outside timing
 ...
 +    result->ok_count = ok_count;
 +
 +    return 0;
++    } catch (...) {
++        return -1;
++    }
 +}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/checksig-census/instrumentation.diff` around lines 151 - 215, Prevent
exceptions from escaping btck_bare_verify_bench by validating count against a
safe implementation limit before reserve or allocation, then wrapping the full
body—including vector construction and verification—in try/catch. Return the
existing failure code for oversized counts and any caught exception, preserving
normal result population and return behavior on success.
🧹 Nitpick comments (1)
tools/checksig-census/instrumentation.diff (1)

306-335: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The 24-counter contract is a magic number on both sides of the FFI with nothing enforcing it. The kernel enum, the kernel constant, and the Rust snapshot buffer each spell out 24 independently. Nothing fails when they diverge; the snapshot just truncates or pads with zeros and the invariant reports success on data it never read.

  • tools/checksig-census/instrumentation.diff#L306-L335: add static_assert(static_cast<size_t>(C_CENSUS_COUNT) == CENSUS_COUNTER_COUNT, ...) so the enum and the constant cannot drift at build time.
  • tools/checksig-census/bare-secp/src/main.rs#L129-L131: size the buffer from COUNTER_NAMES.len(), pass counters.len() to btck_census_snapshot, and bail! when btck_census_counter_count() disagrees.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/checksig-census/instrumentation.diff` around lines 306 - 335, Enforce
the counter-count contract at both sites: in
tools/checksig-census/instrumentation.diff lines 306-335, add a static assertion
that C_CENSUS_COUNT equals CENSUS_COUNTER_COUNT; in
tools/checksig-census/bare-secp/src/main.rs lines 129-131, size the snapshot
buffer from COUNTER_NAMES.len(), pass counters.len() to btck_census_snapshot,
and bail when btck_census_counter_count() differs from the expected count.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/checksig-census/bare-secp/src/main.rs`:
- Around line 129-131: Update the census snapshot logic in main to size counters
from COUNTER_NAMES.len() instead of hardcoding 24, query the native count with
btck_census_counter_count(), and assert that it matches the Rust buffer length
before calling btck_census_snapshot. Preserve the existing snapshot and
invariant checks after validating the count.

In `@tools/checksig-census/capture/src/main.rs`:
- Around line 76-87: Update the capture argument setup around args.counters and
args.journal to resolve each configured path from the corresponding BRS_CENSUS_*
environment variable when the CLI option is absent, validate and prepare the
effective path once, and use it for native sink configuration. Ensure the
summary serialization records these effective paths instead of null when
environment variables configure the sinks.
- Around line 69-87: In tools/checksig-census/capture/src/main.rs lines 69-87,
add canonical path resolution and validation before ensure_parent, native sink
setup, or any artifact creation: compare the corpus, summary, sidecar, counters,
and journal paths and reject aliases that resolve to the same location. In
crates/consensus/examples/kernel_verify_spike.rs lines 78-95, perform the
equivalent corpus-versus-report canonical path comparison before extraction or
report output; both sites must reject conflicting artifact paths before writing.

In `@tools/checksig-census/instrumentation.diff`:
- Around line 953-1000: Reconcile census classification across the Schnorr and
ECDSA instrumentation, using reject_reason and verification outcome rather than
sig_version to identify record types. Ensure only records corresponding to
actual VerifySchnorrSignature or ECDSA verification calls use outcome 0 or 1,
while rejected pre-verification records use the designated reject classification
(including reason 8) so total_records matches the required accounting.

In `@tools/checksig-census/README.md`:
- Around line 106-149: Update the later “Validate census + cross-check” section
to reference the Run A outputs under the c150 names, including
c150.counters.json and c150.journal.bin. Update the “Output artifacts” tree to
list c150.counters.json, c150.contexts.bin, c150.records.bin, c150.journal.bin,
c150.replay.json, and c150.classification.json, removing the obsolete
census-0-150k and census-replay names.

---

Outside diff comments:
In `@tools/checksig-census/instrumentation.diff`:
- Around line 891-943: Replace every direct uint8_t cast of vchSig.size() and
vchPubKey.size() in the census_emit_record calls within CheckECDSASignature with
explicit census_clamp_len limits of 72 and 65 respectively, and propagate a
truncation indicator in the record format so analyzers can distinguish clamped
lengths from exact lengths. Update the README records documentation to describe
the clamping semantics.
- Around line 151-215: Prevent exceptions from escaping btck_bare_verify_bench
by validating count against a safe implementation limit before reserve or
allocation, then wrapping the full body—including vector construction and
verification—in try/catch. Return the existing failure code for oversized counts
and any caught exception, preserving normal result population and return
behavior on success.

---

Nitpick comments:
In `@tools/checksig-census/instrumentation.diff`:
- Around line 306-335: Enforce the counter-count contract at both sites: in
tools/checksig-census/instrumentation.diff lines 306-335, add a static assertion
that C_CENSUS_COUNT equals CENSUS_COUNTER_COUNT; in
tools/checksig-census/bare-secp/src/main.rs lines 129-131, size the snapshot
buffer from COUNTER_NAMES.len(), pass counters.len() to btck_census_snapshot,
and bail when btck_census_counter_count() differs from the expected count.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e4be263-1692-45e5-b0c7-dccf941b0d49

📥 Commits

Reviewing files that changed from the base of the PR and between 4fd333f and 974592e.

📒 Files selected for processing (5)
  • crates/consensus/examples/kernel_verify_spike.rs
  • tools/checksig-census/README.md
  • tools/checksig-census/bare-secp/src/main.rs
  • tools/checksig-census/capture/src/main.rs
  • tools/checksig-census/instrumentation.diff
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: test
  • GitHub Check: bench-smoke
🧰 Additional context used
🔍 Remote MCP Context7, Github Grep

Additional review context

  • Bitcoin Core’s bitcoinkernel target includes the script interpreter and consensus-validation sources; its documentation describes migration into libbitcoin_kernel as ongoing. The new instrumentation therefore sits on an upstream-sensitive boundary where ABI/build compatibility merits verification.
  • Bitcoin Core’s secp256k1 contract accepts only low-S ECDSA signatures; normalization is explicitly described as potentially reintroducing malleability. Census classification should preserve this distinction between rejected and successfully verified signatures.
  • GitHub search found no public matches for the PR-specific symbols (btck_census_*, btck_bare_verify_bench, CHECKSIG_CENSUS, or schnorr_verify_ok), so there is no independent upstream implementation to use for compatibility comparison.
🔇 Additional comments (7)
tools/checksig-census/capture/src/main.rs (1)

177-229: LGTM!

Also applies to: 335-383, 499-503

crates/consensus/examples/kernel_verify_spike.rs (2)

105-194: LGTM!

Also applies to: 239-363, 608-636


198-202: 🎯 Functional Correctness

Do not add a BIP16 height gate. compute_verify_flags intentionally enables P2SH for every block except the hash-pinned exception, and both helpers match this production behavior.

			> Likely an incorrect or invalid review comment.
tools/checksig-census/instrumentation.diff (2)

236-273: LGTM!

Also applies to: 1026-1080


68-106: 🗄️ Data Integrity & Integration

No census guard is needed. Run A builds with kernel by default. Its Rayon workers call check_input, which calls bitcoinkernel::verify for each input. That native API enters btck_script_pubkey_verify, where tl_census is initialized before VerifyScript.

			> Likely an incorrect or invalid review comment.
tools/checksig-census/bare-secp/src/main.rs (1)

30-55: LGTM!

Also applies to: 69-69

tools/checksig-census/README.md (1)

62-62: LGTM!

Also applies to: 175-177, 224-224, 402-402, 435-455

Comment on lines +129 to +131
let mut counters = [0u64; 24];
unsafe {
libbitcoinkernel_sys::btck_census_snapshot(counters.as_mut_ptr(), 22);
libbitcoinkernel_sys::btck_census_snapshot(counters.as_mut_ptr(), 24);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Stop hardcoding 24; ask the kernel.

btck_census_counter_count() exists in the ABI and this code ignores it. If the kernel grows to 26 counters, the snapshot silently reads 24 and INV-15 declares "all counters zero" for counters it never looked at. If the kernel shrinks, census_snapshot copies fewer values and the tail stays zero for the same reason. Either way the invariant lies.

Derive the buffer from COUNTER_NAMES.len() and assert against the native count.

🛡️ Proposed fix
     // Verify counters stayed zero.
-    let mut counters = [0u64; 24];
+    let native_count = unsafe { libbitcoinkernel_sys::btck_census_counter_count() };
+    if native_count != COUNTER_NAMES.len() {
+        bail!(
+            "census counter ABI mismatch: kernel reports {native_count}, tool knows {}",
+            COUNTER_NAMES.len()
+        );
+    }
+    let mut counters = [0u64; COUNTER_NAMES.len()];
     unsafe {
-        libbitcoinkernel_sys::btck_census_snapshot(counters.as_mut_ptr(), 24);
+        libbitcoinkernel_sys::btck_census_snapshot(counters.as_mut_ptr(), counters.len());
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let mut counters = [0u64; 24];
unsafe {
libbitcoinkernel_sys::btck_census_snapshot(counters.as_mut_ptr(), 22);
libbitcoinkernel_sys::btck_census_snapshot(counters.as_mut_ptr(), 24);
// Verify counters stayed zero.
let native_count = unsafe { libbitcoinkernel_sys::btck_census_counter_count() };
if native_count != COUNTER_NAMES.len() {
bail!(
"census counter ABI mismatch: kernel reports {native_count}, tool knows {}",
COUNTER_NAMES.len()
);
}
let mut counters = [0u64; COUNTER_NAMES.len()];
unsafe {
libbitcoinkernel_sys::btck_census_snapshot(counters.as_mut_ptr(), counters.len());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/checksig-census/bare-secp/src/main.rs` around lines 129 - 131, Update
the census snapshot logic in main to size counters from COUNTER_NAMES.len()
instead of hardcoding 24, query the native count with
btck_census_counter_count(), and assert that it matches the Rust buffer length
before calling btck_census_snapshot. Preserve the existing snapshot and
invariant checks after validating the count.

Comment on lines +69 to +87
// Resolve the sidecar path before anything else so we can fail early.
let sidecar_path = args
.context_sidecar
.clone()
.unwrap_or_else(|| resolve_default_sidecar(&args.corpus, args.output.as_ref()));
ensure_parent(&sidecar_path)?;

// The harness is still single-threaded here. Configure the native sinks
// before the kernel or any worker pool can read the process environment.
if let Some(path) = &args.counters {
ensure_parent(path)?;
// SAFETY: no other thread can access the environment before this call.
unsafe { std::env::set_var(CENSUS_COUNTERS_ENV, path.as_os_str()) };
}
if let Some(path) = &args.journal {
ensure_parent(path)?;
// SAFETY: no other thread can access the environment before this call.
unsafe { std::env::set_var(CENSUS_JOURNAL_ENV, path.as_os_str()) };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject artifact-path aliases before writing evidence. Both tools treat caller-provided artifact paths as independent. Aliases can overwrite the corpus or corrupt generated evidence.

  • tools/checksig-census/capture/src/main.rs#L69-L87: resolve and compare the corpus, summary, sidecar, counters, and journal paths before any create or native sink setup.
  • crates/consensus/examples/kernel_verify_spike.rs#L78-L95: resolve and compare the corpus and report paths before extraction or report output.
📍 Affects 2 files
  • tools/checksig-census/capture/src/main.rs#L69-L87 (this comment)
  • crates/consensus/examples/kernel_verify_spike.rs#L78-L95
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/checksig-census/capture/src/main.rs` around lines 69 - 87, In
tools/checksig-census/capture/src/main.rs lines 69-87, add canonical path
resolution and validation before ensure_parent, native sink setup, or any
artifact creation: compare the corpus, summary, sidecar, counters, and journal
paths and reject aliases that resolve to the same location. In
crates/consensus/examples/kernel_verify_spike.rs lines 78-95, perform the
equivalent corpus-versus-report canonical path comparison before extraction or
report output; both sites must reject conflicting artifact paths before writing.

Comment on lines +76 to +87
// The harness is still single-threaded here. Configure the native sinks
// before the kernel or any worker pool can read the process environment.
if let Some(path) = &args.counters {
ensure_parent(path)?;
// SAFETY: no other thread can access the environment before this call.
unsafe { std::env::set_var(CENSUS_COUNTERS_ENV, path.as_os_str()) };
}
if let Some(path) = &args.journal {
ensure_parent(path)?;
// SAFETY: no other thread can access the environment before this call.
unsafe { std::env::set_var(CENSUS_JOURNAL_ENV, path.as_os_str()) };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Record effective native sink paths.

When callers configure BRS_CENSUS_COUNTERS or BRS_CENSUS_JOURNAL directly, args.counters and args.journal remain None. The summary then reports null for artifacts that the kernel actually wrote. This breaks evidence provenance.

Resolve the environment fallback once, validate it, and serialize the effective paths in the summary.

Also applies to: 251-254

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/checksig-census/capture/src/main.rs` around lines 76 - 87, Update the
capture argument setup around args.counters and args.journal to resolve each
configured path from the corresponding BRS_CENSUS_* environment variable when
the CLI option is absent, validate and prepare the effective path once, and use
it for native sink configuration. Ensure the summary serialization records these
effective paths instead of null when environment variables configure the sinks.

Comment on lines +953 to 1000
@@ -1723,21 +1791,43 @@
// abort script execution). This is implemented in EvalChecksigTapscript, which won't invoke
// CheckSchnorrSignature in that case. In other contexts, they are invalid like every other signature with
// size different from 64 or 65.
if (sig.size() != 64 && sig.size() != 65) return set_error(serror, SCRIPT_ERR_SCHNORR_SIG_SIZE);
- if (sig.size() != 64 && sig.size() != 65) return set_error(serror, SCRIPT_ERR_SCHNORR_SIG_SIZE);
+
+ const uint8_t sig_ver_byte = sigversion == SigVersion::TAPSCRIPT ? 2 : 3;
+ const uint8_t captured_sig_len = sig.size() > 72 ? 72 : static_cast<uint8_t>(sig.size());
+ if (sig.size() != 64 && sig.size() != 65) {
+ census_emit_record(tl_census.cur_op, sig_ver_byte, 2, captured_sig_len, 32, 0, 4,
+ nullptr, sig.data(), pubkey_in.data());
+ return set_error(serror, SCRIPT_ERR_SCHNORR_SIG_SIZE);
+ }

XOnlyPubKey pubkey{pubkey_in};
@@ -1994,20 +2057,21 @@ static bool VerifyWitnessProgram(const CScriptWitness& witness, int witversion,
return set_error(serror, SCRIPT_ERR_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM);
}
// Other version/size/p2sh combinations return true for future softfork compatibility
return true;

uint8_t hashtype = SIGHASH_DEFAULT;
if (sig.size() == 65) {
hashtype = SpanPopBack(sig);
- if (hashtype == SIGHASH_DEFAULT) return set_error(serror, SCRIPT_ERR_SCHNORR_SIG_HASHTYPE);
- }
+ if (hashtype == SIGHASH_DEFAULT) {
+ census_emit_record(tl_census.cur_op, sig_ver_byte, 2, 64, 32, hashtype, 5,
+ nullptr, sig.data(), pubkey_in.data());
+ return set_error(serror, SCRIPT_ERR_SCHNORR_SIG_HASHTYPE);
+ }
+ }
+ if (!this->txdata) {
+ census_emit_record(tl_census.cur_op, sig_ver_byte, 2, 64, 32, hashtype, 6,
+ nullptr, sig.data(), pubkey_in.data());
+ return HandleMissingData(m_mdb);
+ }
+
uint256 sighash;
- if (!this->txdata) return HandleMissingData(m_mdb);
if (!SignatureHashSchnorr(sighash, execdata, *txTo, nIn, hashtype, sigversion, *this->txdata, m_mdb)) {
+ census_emit_record(tl_census.cur_op, sig_ver_byte, 2, 64, 32, hashtype, 7,
+ nullptr, sig.data(), pubkey_in.data());
return set_error(serror, SCRIPT_ERR_SCHNORR_SIG_HASHTYPE);
}
// There is intentionally no return statement here, to be able to use "control reaches end of non-void function" warnings to detect gaps in the logic above.
- if (!VerifySchnorrSignature(sig, pubkey, sighash)) return set_error(serror, SCRIPT_ERR_SCHNORR_SIG);
+
+ const bool schnorr_ok = VerifySchnorrSignature(sig, pubkey, sighash);
+ census_emit_record(tl_census.cur_op, sig_ver_byte, schnorr_ok ? 1 : 0, 64, 32, hashtype, 0,
+ sighash.begin(), sig.data(), pubkey_in.data());
+ if (!schnorr_ok) return set_error(serror, SCRIPT_ERR_SCHNORR_SIG);
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether analyze.py invariants account for Schnorr records.
set -euo pipefail

rg -n -C6 'checkecdsa_entries|ecdsa_verify_calls|schnorr' tools/checksig-census/analyze.py

# Do any invariants partition records by sig_version or reject_reason?
rg -n -C4 'sig_version|reject_reason|outcome' tools/checksig-census/analyze.py

Repository: gosuda/bitcoin-rs

Length of output: 12962


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- analyzer counter and record definitions ---'
sed -n '1,210p' tools/checksig-census/analyze.py
printf '%s\n' '--- invariant implementation ---'
sed -n '300,390p' tools/checksig-census/analyze.py

printf '%s\n' '--- instrumentation counter updates and record emission ---'
rg -n -C5 'checkschnorr_entries|schnorr_verify_calls|census_emit_record|reject_reason|sig_ver_byte' tools/checksig-census

Repository: gosuda/bitcoin-rs

Length of output: 33129


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Schnorr entry and counter increments ---'
rg -n -C10 'C_CHECKSCHNORR_ENTRIES|C_SCHNORR_VERIFY_CALLS|C_SCHNORR_VERIFY_OK|C_SCHNORR_VERIFY_FAIL|CheckSchnorrSignature|EvalChecksigTapscript' tools/checksig-census/instrumentation.diff

printf '%s\n' '--- all record-emission sites ---'
rg -n -C3 'census_emit_record' tools/checksig-census/instrumentation.diff

Repository: gosuda/bitcoin-rs

Length of output: 9287


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- tapscript dispatch and signature checker callers ---'
sed -n '730,810p' tools/checksig-census/instrumentation.diff
rg -n -C8 'CheckECDSASignature|CheckSchnorrSignature|EvalChecksigTapscript|pubkey.size\(\)' tools/checksig-census/instrumentation.diff

Repository: gosuda/bitcoin-rs

Length of output: 7240


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

diff = Path("tools/checksig-census/instrumentation.diff").read_text()
analyzer = Path("tools/checksig-census/analyze.py").read_text()

required_source_facts = {
    "ecdsa_entry": "census_inc(C_CHECKECDSA_ENTRIES);",
    "schnorr_entry": "census_inc(C_CHECKSCHNORR_ENTRIES);",
    "schnorr_verify": "census_inc(C_SCHNORR_VERIFY_CALLS);",
    "skip_record": "tl_census.cur_op, 2, 2, 0, 32, 0, 8,",
}
for name, text in required_source_facts.items():
    assert text in diff, f"missing source fact: {name}"

assert "outcome_01 == c.ecdsa_verify_calls" in analyzer
assert "total == c.checkecdsa_entries" in analyzer

# Model one mixed stream:
# ECDSA: 8 verification records + 2 pre-verification rejects.
# Schnorr: 2 verification records + 1 pre-verification reject.
# Tapscript skip: 1 reason-8 record emitted before CheckSchnorrSignature.
ecdsa_verify_calls = 8
checkecdsa_entries = 10
schnorr_verify_calls = 2
checkschnorr_entries = 3
reason8_records = 1

outcome_01 = ecdsa_verify_calls + schnorr_verify_calls
total_records = checkecdsa_entries + checkschnorr_entries + reason8_records

current_inv9 = (
    outcome_01 == ecdsa_verify_calls
    and total_records == checkecdsa_entries
)
corrected_inv9 = (
    outcome_01 == ecdsa_verify_calls + schnorr_verify_calls
    and total_records
    == checkecdsa_entries + checkschnorr_entries + reason8_records
)

print({
    "outcome_01": outcome_01,
    "total_records": total_records,
    "current_inv9_passes": current_inv9,
    "corrected_inv9_passes": corrected_inv9,
})
assert not current_inv9
assert corrected_inv9
PY

Repository: gosuda/bitcoin-rs

Length of output: 256


Reconcile all signature records in INV-9.

count(outcome in {0,1}) must equal ecdsa_verify_calls + schnorr_verify_calls. total_records must equal checkecdsa_entries + checkschnorr_entries + count(reject_reason == 8). Do not partition only by sig_version; ECDSA records also use versions 2 and 3.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/checksig-census/instrumentation.diff` around lines 953 - 1000,
Reconcile census classification across the Schnorr and ECDSA instrumentation,
using reject_reason and verification outcome rather than sig_version to identify
record types. Ensure only records corresponding to actual VerifySchnorrSignature
or ECDSA verification calls use outcome 0 or 1, while rejected pre-verification
records use the designated reject classification (including reason 8) so
total_records matches the required accounting.

Comment on lines +106 to +149
### Run A — authoritative C150 cumulative evidence

```bash
REPO=/home/alpha/exp/bitcoin-rs
EXP=$REPO/tools/checksig-census
C150=/home/alpha/bench-g14/corpora/c150

mkdir -p "$EXP/out"

BRS_CENSUS_COUNTERS=$EXP/out/census-0-150k.counters.json \
BRS_CENSUS_JOURNAL=$EXP/out/census-0-150k.journal.bin \
BRS_CENSUS_LABEL=census-0-150k \
BRS_CENSUS_COUNTERS=$EXP/out/c150.counters.json \
BRS_CENSUS_CONTEXTS=$EXP/out/c150.contexts.bin \
BRS_CENSUS_JOURNAL=$EXP/out/c150.journal.bin \
BRS_CENSUS_RECORDS=$EXP/out/c150.records.bin \
BRS_CENSUS_LABEL=c150 \
taskset -c 0-31 \
"$EXP/target/release/examples/mainnet_prefix_replay" \
--stop-height 150000 \
--rest-url 127.0.0.1:18443 \
--blocks-file "$C150/blocks.dat" \
--corpus-manifest "$C150/manifest.json" \
--assume-valid-height 0 \
--data-dir "$EXP/out/census-datadir" \
--output "$EXP/out/census-replay.json"
--data-dir "$EXP/out/c150-datadir" \
--output "$EXP/out/c150.replay.json"

cd "$EXP"
python3 analyze.py classify-corpus \
--counters out/c150.counters.json \
--contexts out/c150.contexts.bin \
--records out/c150.records.bin \
--journal out/c150.journal.bin \
--replay out/c150.replay.json \
--corpus-manifest "$C150/manifest.json" \
--archive "$C150/blocks.dat" \
--output out/c150.classification.json \
--contract c150
```

The replay produces the authoritative `c150.counters.json`, `c150.contexts.bin`,
`c150.journal.bin`, and `c150.records.bin` artifacts in one process. The strict
classifier validates each stream's magic and framing, the exact native count
equations, and every context, record, and journal join.

Expected: `ffi_verify_entries == 2,868,199`, all verdicts true, pre-taproot
counters (`op_checksigadd`, `checkschnorr_entries`, `schnorr_verify_calls`) zero.
counters (`op_checksigadd`, `checkschnorr_entries`, `schnorr_verify_calls`,
`schnorr_verify_ok`, `schnorr_verify_fail`) zero.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run A was renamed to c150.* and the rest of the README was not.

This block writes out/c150.counters.json, out/c150.journal.bin, and out/c150.replay.json. Two later sections still name the old files:

  • The "Validate census + cross-check" command reads out/census-0-150k.counters.json and out/census-0-150k.journal.bin.
  • The "Output artifacts" tree lists census-0-150k.counters.json Run A counters, census-0-150k.journal.bin, and census-replay.json, and lists no contexts, records, or classification output for Run A.

Anyone running the README top to bottom hits a missing file. Rename both sections and add c150.contexts.bin, c150.records.bin, and c150.classification.json to the artifact tree.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/checksig-census/README.md` around lines 106 - 149, Update the later
“Validate census + cross-check” section to reference the Run A outputs under the
c150 names, including c150.counters.json and c150.journal.bin. Update the
“Output artifacts” tree to list c150.counters.json, c150.contexts.bin,
c150.records.bin, c150.journal.bin, c150.replay.json, and
c150.classification.json, removing the obsolete census-0-150k and census-replay
names.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 974592e7f2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

--stop-height 150000 \
--rest-url 127.0.0.1:18443 \
--blocks-file "$C150/blocks.dat" \
--corpus-manifest "$C150/manifest.json" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the unsupported corpus-manifest argument

Following the documented authoritative Run A always stops here with unknown argument: --corpus-manifest. I checked the argument parser and usage string in crates/node/examples/mainnet_prefix_replay.rs:326-365; they support --blocks-file but contain no --corpus-manifest option. Either add that option to the example or remove it from this invocation so the evidence capture can start.

Useful? React with 👍 / 👎.

--output "$EXP/out/c150.replay.json"

cd "$EXP"
python3 analyze.py classify-corpus \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Implement the documented classify-corpus command

The prescribed post-replay step cannot run: analyze.py classify-corpus --help returns invalid choice, and the repo-wide analyzer parser in tools/checksig-census/analyze.py:1223-1263 registers only validate-capture, validate-census, and verdict. Consequently none of the newly claimed context/record/journal joins can be validated until this command is implemented or the workflow uses an existing command.

Useful? React with 👍 / 👎.

Comment on lines +454 to +455
**Counters JSON**: schema=1, label, 24 named u64 counters, record_count,
journal_count, context_count.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reconcile the durable census contract documentation

This changes the durable counter contract to 24 counters, while docs/solutions/performance/checksig-census-and-the-script-check-floor.md:11-15 and its INV-15 statement at line 54 still define the result as a 22-counter census. That leaves the documented project learning contradictory; update the overlapping solution and reconcile the affected script-check-floor vocabulary in CONCEPTS.md as required.

AGENTS.md reference: AGENTS.md:L5-L7

Useful? React with 👍 / 👎.

Comment on lines +995 to +997
+ const bool schnorr_ok = VerifySchnorrSignature(sig, pubkey, sighash);
+ census_emit_record(tl_census.cur_op, sig_ver_byte, schnorr_ok ? 1 : 0, 64, 32, hashtype, 0,
+ sighash.begin(), sig.data(), pubkey_in.data());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude Schnorr records from the ECDSA benchmark

When a capture includes Taproot/Tapscript checks, this emits ordinary outcome 0/1 records containing a 64-byte Schnorr signature and a 32-byte x-only key. bare-secp/src/main.rs:68-90 filters only outcome 2 and ignores sig_version, so it passes these records to the ECDSA-only CPubKey::Verify benchmark; failed Schnorr records can silently appear as expected ECDSA failures and skew timing, while successful ones produce mismatches. Filter the bare input set to ECDSA signature versions or provide a separate Schnorr comparator.

Useful? React with 👍 / 👎.

Comment on lines +115 to +118
BRS_CENSUS_COUNTERS=$EXP/out/c150.counters.json \
BRS_CENSUS_CONTEXTS=$EXP/out/c150.contexts.bin \
BRS_CENSUS_JOURNAL=$EXP/out/c150.journal.bin \
BRS_CENSUS_RECORDS=$EXP/out/c150.records.bin \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update downstream steps to use the renamed C150 artifacts

Run A now writes c150.counters.json, but the later validate-census command at tools/checksig-census/README.md:296-300 still opens out/census-0-150k.counters.json and the old journal name. In a clean run those files do not exist, so the documented validation sequence fails with a file-not-found error; use the new C150 names consistently in downstream commands and the artifact inventory.

Useful? React with 👍 / 👎.

Comment on lines +70 to +73
let sidecar_path = args
.context_sidecar
.clone()
.unwrap_or_else(|| resolve_default_sidecar(&args.corpus, args.output.as_ref()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject colliding capture paths

The sidecar path is accepted without checking it against the corpus, summary, or native sink paths. If --context-sidecar equals the corpus, File::create at line 114 truncates the input corpus after loading it; if the default derived from an output such as capture.context.jsonl equals that output, the final summary write overwrites the completed sidecar and leaves the reported sidecar hash invalid. Validate that every input and output path is distinct before creating any file.

Useful? React with 👍 / 👎.

Comment on lines +53 to +54
"schnorr_verify_ok",
"schnorr_verify_fail",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align the verdict analyzer with the 24-counter output

After Run C starts emitting these two additional counters, the documented analyze.py verdict step rejects every new bare-secp result. Its COUNTER_NAMES at tools/checksig-census/analyze.py:36-59 still contains 22 names, and lines 995-1003 require the inv_15.counters key set to match that list exactly, so schnorr_verify_ok and schnorr_verify_fail are reported as unexpected extras. Update the analyzer's counter contract before producing 24-key results.

Useful? React with 👍 / 👎.

@metaphorics
metaphorics merged commit 38b4318 into main Aug 18, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Capture cumulative script evidence

1 participant