Skip to content

fix(types): declare input_hash/output_hash on ReceiptPayload#19

Open
web3guru888 wants to merge 1 commit into
fetchai:mainfrom
web3guru888:fix/receiptpayload-hash-typing
Open

fix(types): declare input_hash/output_hash on ReceiptPayload#19
web3guru888 wants to merge 1 commit into
fetchai:mainfrom
web3guru888:fix/receiptpayload-hash-typing

Conversation

@web3guru888

Copy link
Copy Markdown

fix(types): declare input_hash/output_hash on ReceiptPayload

What

In proof_only mode ReceiptBuilder.build redacts inputs/output to None
and adds input_hash/output_hash SHA-256 digests
(src/aevs/core/receipt.py:137-139):

if cfg.receipt_visibility == "proof_only":
    receipt["input_hash"] = input_hash
    receipt["output_hash"] = output_hash

…but those two keys were absent from the ReceiptPayload TypedDict
(src/aevs/core/types.py). Type checkers therefore could not see the keys a
proof_only receipt actually carries, and the gap was masked behind a
# type: ignore[return-value] on the return statement.

The fix

Declare both keys as optional schema members:

class ReceiptPayload(_ReceiptRequired, total=False):
    input_hash: str
    output_hash: str

They are optional because only proof_only receipts carry them — private
and public receipts omit them entirely.

Why the TypedDict-inheritance idiom (not NotRequired)

The project targets python = "^3.10". typing.NotRequired is 3.11+, and
typing_extensions is not a declared dependency. The native, dependency-free
way to express per-key optionality on 3.10 is a required base TypedDict plus a
total=False subclass — which is what this PR uses. ReceiptPayload remains
the single public name.

About the # type: ignore[return-value] — a finding

The ignore did not stem from the missing keys. The receipt is built
incrementally as a dict[str, Any] (so the conditional proof_only keys and the
post-hoc payload_hmac can be appended), and mypy rejects returning a
dict[str, Any] where a TypedDict is expected:

src/aevs/core/receipt.py:162: error: Incompatible return value type
    (got "dict[str, Any]", expected "ReceiptPayload")  [return-value]

So simply deleting the comment re-surfaces that error (see
01-ignore-is-load-bearing-on-main.txt). It is replaced with an explicit,
type-checked cast(ReceiptPayload, receipt) — a typed boundary assertion at
the end of the builder rather than a blanket line-level error suppression. The
signing/chain bytes are unchanged: the dict is still canonicalized and
signed without payload_hmac exactly as before, so existing signature/chain
tests continue to pass byte-for-byte.

Tests

New tests/test_receipt.py::TestReceiptPayloadSchema (5 cases):

Test Asserts
test_hash_fields_are_optional_schema_keys input_hash/output_hash__optional_keys__
test_hash_fields_are_not_required …and ∉ __required_keys__
test_builder_keys_are_declared_in_schema every key a proof_only receipt emits is declared
test_proof_only_emits_the_optional_keys proof_only receipts carry both keys
test_default_visibility_omits_optional_keys private receipts omit both keys

The first and third fail on main (keys absent from the schema); all pass with
this change (see 02-schema-tests-fail-before-fix.txt).

Verification (make check)

  • pytest: 497 passed, 2 skipped (492 baseline + 5 new) — 03-full-suite.txt
  • ruff check: clean — 04-ruff.txt
  • mypy --strict: clean, 22 files — 05-mypy.txt

Note: the full suite requires the declared dev-deps (langgraph,
pytest-asyncio in [tool.poetry.group.dev.dependencies]) installed; with
only the langchain,mcp extras, tests/test_invocation_id.py errors on
import langgraph — an environment quirk unrelated to this change.

Closes the type-checker blind spot for proof_only receipts.

In `proof_only` mode `ReceiptBuilder.build` redacts `inputs`/`output` to
`None` and adds `input_hash`/`output_hash` SHA-256 digests
(receipt.py:137-139), but those two keys were absent from the
`ReceiptPayload` TypedDict. As a result type checkers could not see the
keys a proof_only receipt actually carries (e.g. `receipt["input_hash"]`
was a typing blind spot), and the discrepancy was hidden behind a
`# type: ignore[return-value]` on the return statement.

Declare both keys as optional schema members. To stay Python 3.10
compatible (the project targets `python = "^3.10"`, and `typing.NotRequired`
is 3.11+ while `typing_extensions` is not a declared dependency), the
optionality is expressed with the native TypedDict-inheritance idiom: a
required base plus a `total=False` subclass — no new dependency.

The `# type: ignore[return-value]` actually covered the
`dict[str, Any] -> ReceiptPayload` builder boundary (the receipt is built
incrementally as a plain dict so the conditional proof_only keys and the
post-hoc payload_hmac can be appended), which is independent of the
missing keys. Removing the comment alone re-surfaces that error, so it is
replaced with an explicit, type-checked `cast(ReceiptPayload, receipt)` —
a typed boundary assertion rather than blanket error suppression. The
signing/chain bytes are unchanged (the dict is still signed without
payload_hmac, exactly as before).

Adds `TestReceiptPayloadSchema` regression coverage: the hash keys must be
optional schema members (not required), every key a proof_only receipt
emits must be declared, and private receipts must omit the optional keys.

Signed-off-by: Robin Dey <robin@vbrl.ai>
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.

1 participant