Problem
Synapse.required_hash_fields defaults to () (bittensor/core/synapse.py). body_hash returns get_hash("".join(hashes)), and when required_hash_fields is empty, hashes stays [], so body_hash is get_hash("") - a constant independent of the body. Any Synapse subclass that does not set required_hash_fields therefore has a no-op integrity check: completely different payloads produce the same computed_body_hash.
Verified on staging (Python 3.12, bittensor 10.5.0)
class A(Synapse): name: str; data: str # required_hash_fields stays () (default)
A(name='route1', data='alpha').body_hash
-> a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a
A(name='route2', data='TOTALLY DIFFERENT BODY CONTENT 12345').body_hash
-> a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a # IDENTICAL
# once required_hash_fields = ('data',) is set, the hashes differ (integrity works)
(a7ffc6f8... is the SHA3-256 of the empty string.)
Why not a simple PR
Hashing-all-fields-when-empty is wire-incompatible: a new dendrite that hashes the full body would no longer match an old axon that hashes the empty string, so every mixed-version request would be rejected. The only wire-safe code change is emitting a runtime warning when required_hash_fields is empty, which does not close the hole. That is why I am opening this for design rather than sending a patch.
Suggested direction (for discussion)
A versioned body-hash header (e.g. body_hash_v2 computed over the full serialized body), negotiated by protocol/feature version, so new<->new traffic gets real integrity while new<->old keeps the current (empty) behaviour. Optional interim hardening: warn loudly when a Synapse used for signing has an empty required_hash_fields.
Related prior work on required_hash_fields (all closed) addressed adding/using the field, not this empty-default fail-open.
Problem
Synapse.required_hash_fieldsdefaults to()(bittensor/core/synapse.py).body_hashreturnsget_hash("".join(hashes)), and whenrequired_hash_fieldsis empty,hashesstays[], sobody_hashisget_hash("")- a constant independent of the body. AnySynapsesubclass that does not setrequired_hash_fieldstherefore has a no-op integrity check: completely different payloads produce the samecomputed_body_hash.Verified on
staging(Python 3.12, bittensor 10.5.0)(
a7ffc6f8...is the SHA3-256 of the empty string.)Why not a simple PR
Hashing-all-fields-when-empty is wire-incompatible: a new dendrite that hashes the full body would no longer match an old axon that hashes the empty string, so every mixed-version request would be rejected. The only wire-safe code change is emitting a runtime warning when
required_hash_fieldsis empty, which does not close the hole. That is why I am opening this for design rather than sending a patch.Suggested direction (for discussion)
A versioned body-hash header (e.g.
body_hash_v2computed over the full serialized body), negotiated by protocol/feature version, so new<->new traffic gets real integrity while new<->old keeps the current (empty) behaviour. Optional interim hardening: warn loudly when a Synapse used for signing has an emptyrequired_hash_fields.Related prior work on
required_hash_fields(all closed) addressed adding/using the field, not this empty-default fail-open.