Skip to content

devshard (refactoring) : unify host signature identity - #1791

Open
a-kuprin wants to merge 3 commits into
devshard-0.2.x-v6from
ak/sig-auth-refactoring
Open

a-kuprin wants to merge 3 commits into
devshard-0.2.x-v6from
ak/sig-auth-refactoring

Conversation

@a-kuprin

@a-kuprin a-kuprin commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Unify host signature identity

This PR is three things only: unify identity, fix the bugs the old split allowed, and make proto preimage collisions fail CI instead of shipping as replay.


1. Refactoring for unification

Host authentication has two halves. They were mixed; they are not mixed now.

Half Question Where it lives
Preimage What bytes were signed? One helper per message family (CanonicalAckBytes, CanonicalSignedBytes, …)
Identity May this recovered address act for this slot? signing.SlotActors.Allows only

There is still one ecrecover (signing.Verifier). There is no second crypto path. The refactor deletes per-message identity forks (IsWarmKeyForSlot, hand-written HeightAck cold-or-warm, settlement’s own map walk, repair’s own slot-key check).

Allows is the same sequence everywhere:

  1. Cold key for the slot.
  2. If WarmKeys[slot] is set: that binding is exclusive (cold or that warm). Stop.
  3. If the slot is unbound: a warm key already bound on a sibling slot of the same validator (authz is per-address).
  4. Else AcceptWarm (live authz). Nil = state only.

Steps 1–3 are consensus state. Only step 4 may touch the chain, and only for an unbound slot.

Callers do not build SlotActors inline. One constructor per mutation right:

Context Constructor Bind WarmKeys?
Apply hostSignerAllowedLockedResolveWarmKey yes
Read-only SM hostSignerCachedLocked no
Host / transport host.SlotActors() (request-scoped memo) no (CheckWarmKey)
Log-plane L2 LogPlaneState.actors() no (CheckWarmKey)

HeightAck apply is not special. L2 still uses CheckWarmKey because compose trials prefixes that can be dropped. applyHeightAck uses hostSignerAllowedLocked, same as confirm / finish / votes. HTTP admission and user-diff owner match stay outside SlotActors.

New signed messages reuse this: one canonical-bytes helper, recover with Verifier, identity via the table above. Do not add a new “is this a warm key” helper.


2. Bugs fixed

These were real, not style.

HeightAck identity ≠ confirm/finish identity. Same ecrecover, different “who may sign.” Ack accepted cold or warm depending on which branch you hit; confirm used cached warm + sibling + bind-on-apply. A heartbeat can name a slot that has never executed, so the fork showed up as INVALID acks, Testermint hangs, and first-time / sibling warm acks that compose admitted and applyCore rejected.

applyHeightAck fetched warm keys from chain as a side path. After L2 already admitted the signer it did if recovered != cold { ResolveWarmKey(...) } and ignored the result. Consequences:

  • A sibling ack (already decided from WarmKeys) still called the bridge.
  • Success could write a second slot binding, so two replicas whose bridges disagree could seal different rest hashes.
  • Failure still applied the tx.

Fixed: hostSignerAllowedLocked. Sibling stays state-only and does not write WarmKeys[slot]. Failed resolution is an apply error.

Bound slot was not exclusive on every path. A sibling warm key, or a live authz answer, could override a binding already in state. Allows stops at step 2 when WarmKeys[slot] is set. Covered by TestApplyConfirmStart_BoundSlotRejectsSiblingWarmKey / sibling HeightAck tests (no bridge, no second binding).

Signature replay across proto types. Protobuf does not encode the message type name. Two messages with the same field numbers and wire types marshal to the same bytes, so a signature over one verified as the other:

Signed as Replayed as Why the bytes matched
MsgValidation (valid) MsgValidationVote (vote_valid) Identical layout (fields 1–3)
TimeoutVoteContent with reason omitted (proto3) ErrorMissVoteContent (no field 4) Fields 1–3 identical; reject vs accept

Fixed by domain prefixes on those four families (devshard.validation.v1, devshard.validationvote.v1, devshard.timeoutvote.v1, devshard.errormissvote.v1). Protocol break for those families only: existing signatures will not verify. Do not mix this binary with a v5 signer on an escrow that already has those votes. Height-sync tags and unprefixed finish / receipt / state / user diffs are unchanged.

Not fixed here: sticky WarmKeys after bind / rotate / revoke (warm-key-revoke.md).


3. Automation against proto replay

Domain tags fix the collisions we know. CI must reject the next one.

TestSignedProtoPreimagesAreNotInterchangeable (and siblings in devshard/types/signed_preimage_test.go):

  1. Walk every message in proto/devshard/v1.
  2. Require it classified signed or unsigned. An unclassified new proto fails the test.
  3. For signed messages in the same domain: fail if field layouts are equal or one is a subset of the other (including proto3-omissible suffix fields).
  4. Require domain tags unique; CanonicalSignedBytes rejects a nil message.

make test runs this. The unit-test workflow has a named step so a collision is its own check, not a buried go test ./... failure:

Check signed proto preimages are not interchangeable
go test ./types/ -run 'TestAllDevshardProtoMessagesClassified|TestSignedProtoPreimagesAreNotInterchangeable|TestCanonicalSignedBytesRejects|TestSignedPreimageDomainsAreUnique' -count=1 -v

Adding a signed proto without a unique domain or a disjoint layout cannot merge.


Test plan

  • go test ./signing/ ./types/ ./state/ ./heightsync/ ./host/ ./transport/ ./user/
  • Named preimage tests (same -run as CI)
  • Sibling warm HeightAck and ConfirmStart: zero bridge calls, no second WarmKeys entry
  • First-time warm ack binds; ValidateDiff carries WarmKeyDelta
  • Bound slot rejects sibling warm; cold key still acts
  • Validation signature does not verify as MsgValidationVote; timeout reject does not verify as error-miss accept
  • Repair / gossip / settlement still accept cold, bound warm, and sibling

…air, confirm, finish, votes, gossip, and settlement all apply the same cold/warm/sibling rule.

Bound slots stay exclusive; sibling warm keys are derived from consensus state without a bridge call or extra binding, and new endpoints should reuse SlotActors rather than a parallel identity check.
…signature cannot be replayed as another message type.

Protobuf omits the type name, so MsgValidation/MsgValidationVote and timeout/error-miss contents could marshal identically; CanonicalSignedBytes prefixes unique tags and CI rejects interchangeable layouts.
…erAllowedLocked like confirm/finish so sibling acks stay state-only and a failed resolution cannot apply.
@a-kuprin a-kuprin added this to the v0.2.x-devshard6 milestone Sep 17, 2026
@tcharchian tcharchian mentioned this pull request Sep 22, 2026
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.

2 participants