Skip to content

Route `StateSignatureContent` through `CanonicalSignedBytes` #1939

Open @a-kuprin opened 2026-10-07 14:51 UTC 0 comments Updated 2026-10-07 14:51 UTC
enhancement

Route StateSignatureContent through CanonicalSignedBytes

Status: Follow-up, not blocking
Source: PR #1791 review (kaileido)
Related: PR #1791 — unify host signature identity

Pre-existing. Not introduced by the identity refactor. No divergence on current messages.


Problem

StateSignatureContent is the secp256k1 preimage for a host state attestation (state_root, escrow_id, nonce). Signers and gossip verifiers build it with plain proto.Marshal. Settlement builds the same message with the deterministic marshal.

Those encodings are the same bytes today. The proto is three scalars (bytes, string, uint64) and has no map or other unordered field. proto.Marshal and proto.MarshalOptions{Deterministic: true} agree on that shape.

They stop agreeing if a map, or any field whose wire order is not fixed, is added later. The host would sign one encoding. VerifySettlement and the chain settlement keeper would recover against another. Nodes would reject honest state signatures.

signProposer already goes through types.CanonicalSignedBytes. signState does not.

Call sites

Role Where Encoding now
Sign Host.signState proto.Marshal
Gossip verify Host.AccumulateGossipSig proto.Marshal
Gossip verify Server.HandleGossipNonce proto.Marshal
User verify Session.verifyStateSignature proto.Marshal
Off-chain settlement state.VerifySettlement deterministicMarshal
On-chain settlement keeper over DevshardStateSignatureContent gogo XXX_Marshal(..., deterministic=true)

Tests that forge the same preimage with proto.Marshal (host, transport, user, state, protocol) follow the signer they copy.

Proposed change

Route every devshard signer and verifier of StateSignatureContent through types.CanonicalSignedBytes:

  • Host.signState
  • Host.AccumulateGossipSig
  • Server.HandleGossipNonce
  • Session.verifyStateSignature
  • state.VerifySettlement

Update the tests that build this preimage by hand so they call the same helper.

StateSignatureContent stays in the empty domain of signedPreimageDomain. CanonicalSignedBytes then returns the deterministic proto body and nothing else. That matches VerifySettlement and the chain keeper on the message as it exists now, so existing signatures still verify. Do not add a domain tag in this change. A tag is a protocol break for every stored state signature, and the chain verifies the unprefixed deterministic encoding of DevshardStateSignatureContent.

Leave the chain type where it is. It is a separate gogo message with the same three fields. This change only makes the devshard side share one helper, so a later unordered field is encoded once.

Out of scope

  • A domain prefix on state signatures, finish, receipt, or user diffs.
  • Replacing DevshardStateSignatureContent with the devshard proto.
  • Sticky WarmKeys after bind / rotate / revoke.

Acceptance

  • signState, both gossip verifiers, verifyStateSignature, and VerifySettlement call CanonicalSignedBytes. No remaining proto.Marshal of StateSignatureContent in non-test production code.
  • CanonicalSignedBytes of a populated StateSignatureContent equals proto.Marshal of the same message (scalar layout, empty domain). A test locks that equality so a domain tag or a non-deterministic field fails CI before it ships.
  • Existing state signatures still verify in VerifySettlement and in the chain keeper. No new domain string.
  • go test ./host/ ./transport/ ./user/ ./state/ ./protocol/ ./types/

🔄 Auto-synced from Issue #1939 every hour.