Skip to content

fix(kernel): export the entry hash that v2 chain links use - #2012

Merged
joshuajbouw merged 2 commits into
astrid-runtime:mainfrom
unicity-aos:fix/audit-export-v2-entry-hash
Sep 30, 2026
Merged

joshuajbouw merged 2 commits into
astrid-runtime:mainfrom
unicity-aos:fix/audit-export-v2-entry-hash

Conversation

@MastaP

@MastaP MastaP commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Linked Issue

Closes #2011

Follow-up to #1996 and #2005.

Summary

audit.export reported every entry's content_hash_hex as BLAKE3(signing_data). For a format-v1 entry that is its content hash. For a format-v2 entry the signing data is the signature wrapper of the entry hash, while chain links, chain heads and prune receipts use the SHA-256 entry hash of the canonical body. On a chain switched to v2, an exported entry's hash therefore matched neither the next entry's previous_hash_hex nor the head in audit.heads, and a verifier following the export could not link the chain.

The export now reports AuditEntry::content_hash(), which is unchanged for v1 entries.

Changes

  • astrid-kernel: audit.export. content_hash_hex is the entry's content hash in either format: BLAKE3(signing_data) for v1, the SHA-256 entry hash for v2.
  • astrid-core: export wire types. Their documentation states which hash each format exports, that v2 signing data is the signature wrapper of that hash, and that only a v1 signature covers whole seconds.
  • Changelog fragment.

Verification

  • New kernel test export_reports_the_hash_that_links_a_chain_switched_to_v2. It exports a chain with two v1 entries followed by three v2 entries and checks:

    • each exported hash against the stored entry;
    • the signature over the exported signing data;
    • that each v2 entry's signing data wraps its entry hash;
    • the links across the switch;
    • the head in the export page and in audit.heads.

    It fails on the previous hash computation (the v2 hashes differ) and passes with this change.

  • Test suites: astrid-core 391 pass. The kernel lib passes 647 of 648 with --test-threads=6 and umask 0022. The remaining failure needs a copy-on-write workspace backend and also fails on main.

  • Lint and changelog: cargo fmt, cargo clippy --all-features --all-targets -- -D warnings on astrid-core and astrid-kernel, and scripts/changelog.py check.

AI / Tool Assistance

Assisted-by: Claude Code:claude-opus-5-5. Covers the fix, the test, the documentation and this description.
Assisted-by: Codex CLI. Review pass over the diff.

Checklist

  • Linked to an issue
  • Changelog fragment added under changes/{issue}.{kind}.md (docs/CI-only may skip; release PRs roll fragments into the version section instead of adding one)
  • I understand every change in this PR and can explain its design, risks, and validation.
  • I reviewed and tested any meaningful tool-generated output included in this PR.
  • Every non-bot, non-merge commit has a matching Signed-off-by trailer.

audit.export reported every entry's content hash as BLAKE3 of its signing
data. For a format-v1 entry that is the content hash. For a format-v2
entry the signing data is the signature wrapper of the entry hash, while
chain links, chain heads and prune receipts use the SHA-256 entry hash of
the canonical body. On a chain switched to v2, an exported entry's
content_hash_hex therefore matched neither the next entry's
previous_hash_hex nor the head hash in audit.heads, and a verifier
following the export could not link the chain.

The export now reports AuditEntry::content_hash(), which is unchanged
for v1 entries. The wire-type documentation states the hash for each
format and that a v1 signature covers whole seconds only.

A kernel test exports a chain with two v1 entries followed by three v2
entries and checks each hash against the stored entry, the v2 signing
wrapper, the links across the switch, and the head in the page and in
audit.heads. It fails on the previous hash computation.

Signed-off-by: Pavel Grigorenko <pavel@unicity-labs.com>
Changelog fragment for astrid-runtime#2011.

Signed-off-by: Pavel Grigorenko <pavel@unicity-labs.com>
Copilot AI balanced review requested due to automatic review settings September 30, 2026 11:10
@MastaP
MastaP requested a review from joshuajbouw as a code owner September 30, 2026 11:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@MastaP

MastaP commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

@joshuajbouw this is the v2 export-hash interaction flagged on #2005 (#2005 (comment)); it wasn't in the merged branch, so here it is on current main.

@joshuajbouw

Copy link
Copy Markdown
Member

Verified on 8ded51b: the mixed v1/v2 export regression passes. Restoring only the old BLAKE3(signing_data) calculation makes the unchanged test fail on the exported content hash, as expected. Restored the candidate and ran the full audit admin group: 9 passed, including paging, pruning, signed heads and access checks. Working tree is clean. The fix uses the existing format-aware AuditEntry::content_hash() without changing signature bytes or wire fields. Fork CI is approved and running; this is local verification, not a completed CI claim.

@MastaP

MastaP commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

@joshuajbouw

The failing check on PR #2012 is Runtime E2E. It's a flaky daemon shutdown in the test harness, not our change

@joshuajbouw

Copy link
Copy Markdown
Member

The failed Runtime E2E run stopped after final registry removal, before the restart check. The saved final-remove response confirms the registry was absent; the shutdown helper then returned failure without recording the child exit status. That helper is unchanged by this PR. The underlying shutdown cause is not yet established.

I preserved the first-run evidence and started one exact-head rerun to check repeatability. Separately, #2016 adds exit-status/forced-kill diagnostics without relaxing the shutdown check. A passing rerun would not establish that the original shutdown problem is fixed.

@joshuajbouw joshuajbouw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed 8ded51b. The mixed-format export regression passes, fails when only the old hash calculation is restored, and the full audit admin group passes (9 tests). Export now uses the same canonical content hash as chain links. The exact-head Runtime E2E rerun passed. The earlier daemon shutdown failure is preserved separately; a green rerun does not establish its cause or fix it.

@joshuajbouw
joshuajbouw merged commit 8108279 into astrid-runtime:main Sep 30, 2026
33 of 34 checks passed
@joshuajbouw
joshuajbouw deleted the fix/audit-export-v2-entry-hash branch September 30, 2026 14:10
joshuajbouw added a commit that referenced this pull request Sep 30, 2026
## Linked Issue

Closes #2015.

## Summary

Report why the runtime harness fails while stopping its daemon. This
does not fix or waive the shutdown failure observed in #2012.

## Changes

- Report PID, elapsed seconds, child exit status and whether the
force-kill branch ran.
- Preserve the existing wait, failure return and PID cleanup.
- Exercise clean exit, nonzero exit and timeout using stubbed process
operations; run these tests in the existing contract suite.
- Do not print runtime logs or secrets.

## Verification

Before the change, two of the three unchanged tests failed because
failure diagnostics were empty. After the change all three pass. Bash
syntax, ShellCheck and git diff --check pass. These tests prove the
reporting contract, not the root cause of the real daemon failure. The
full scripts/ci/test-release-contracts.sh suite also passes. Independent
review accepted c4b7699 and reproduced the parent/head
failure-reporting difference. Follow-up 3d01e42 addresses Copilot's
test-coverage finding: both failure branches now require the correct
PID, numeric elapsed seconds, exit status and force-kill flag, and clean
exit requires empty stderr. All three focused tests pass on the
follow-up. Required CI is pending on the new head.

## Test Plan

Run python3 scripts/test_runtime_shutdown_diagnostics.py and shellcheck
scripts/e2e/runtime-process-helpers.sh. On a future failing real harness
run, use the emitted exit status and force-kill field to distinguish
early daemon failure from a hung process.

## AI / Tool Assistance

Assisted-by: Codex

AI assistance implemented the diagnostics and tests. The complete diff
was reviewed and the executable regression was run before and after the
change.

## Checklist

- [x] Linked to an issue
- [x] CI-only change; no product changelog required
- [x] Reviewed the complete diff
- [x] Executed the focused regression
- [x] Signed commit with matching DCO trailer

---------

Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
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.

audit.export reports the wrong content hash for format-v2 entries

3 participants