Skip to content

feat(airc): add signing_pubkey_hex to AircPeerManifest (L1-6 verify substrate)#1451

Merged
joelteply merged 2 commits into
canaryfrom
feat/airc-peer-manifest-signing-pubkey
May 26, 2026
Merged

feat(airc): add signing_pubkey_hex to AircPeerManifest (L1-6 verify substrate)#1451
joelteply merged 2 commits into
canaryfrom
feat/airc-peer-manifest-signing-pubkey

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Summary

Adds the missing signing_pubkey_hex field to AircPeerManifest so the L1-6 contract event chain can do peer_id → pubkey lookups at verify time. The substrate's trust answer becomes "the manifest IS the directory" — no separate keyring, no out-of-band cert exchange.

Why

L1-6 #1448 shipped Phase A — ed25519 signing + 8-event contract chain + envelope verify. verify() returns the pubkey that signed; the caller must cross-check it against an external trust source to confirm signer identity matches proposer_id / bidder_id / etc.

That cross-check needs a peer_id → pubkey directory. L1-4 #1446 landed AircPeerManifest (peer_id, capabilities, room_ids, timestamps) but no pubkey. This PR completes that surface.

What this lands

  • AircPeerManifest.signing_pubkey_hex: String — required field. 32-byte ed25519 public key, hex-encoded (64 chars, no 0x prefix). Matches SignedContractEvent::signer_pubkey_hex byte-for-byte — same encoding, no transcoding when L1-6 Phase B parses one for verify.
  • AircPeerManifest::validate() — validates the field structurally (length + hex chars). Curve-membership / point-on-line is delegated to ed25519_dalek when a consumer parses the bytes.
  • AircPeerManifestError enum (EmptyPeerId / PubkeyWrongLength / PubkeyNonHexChar) — specific variants so the inbound L1-2 subscriber can log + reject with actionable diagnostics rather than a generic "bad manifest". Per the never-swallow-evidence rule.
  • ts-rs auto-export updates shared/generated/airc/AircPeerManifest.ts with the new field as required signingPubkeyHex: string.

Why required (not optional)

AircPeerManifest is a new type — L1-4 just landed yesterday. No existing manifest traffic to migrate. Making the pubkey required from day one means it's structurally impossible to advertise a peer without a verifiable identity. Optional now → "trust me, my next manifest will have one" later → forever optional. Required is the correct default for a substrate trust directory.

Key rotation

Doc-commented on the type: a peer that mutates its own pubkey publishes a fresh manifest; receivers that already have one for that peer_id reject the mismatch loud. Key rotation has to go through a proper trust-rotation event class, not silent overwrite. Tracking the rotation event class is L4 work; the no-silent-overwrite stance is set today.

Tests (6 new + all 38 realtime tests pass)

  • validates_well_formed_pubkey
  • accepts_uppercase_hex — substrate must NOT reject otherwise valid uppercase just for case
  • rejects_wrong_length_pubkey
  • rejects_non_hex_pubkey
  • rejects_empty_peer_id
  • round_trips_through_json_with_pubkey — verifies camelCase wire form (signingPubkeyHex) + serde round-trip

Bonus fold-in

Generated TS barrel shared/generated/airc/index.ts picks up 3 backfill entries (AircCapabilityIndexEntry, AircPeerCapability, AircPeerManifest) that L1-4's generator pass missed. Pure barrel re-export drift; harmless to include here.

Follow-up

L1-6 Phase B PR will add ContractVerifyingKey::from_hex (or similar) so the verify path reads signing_pubkey_hex straight from the manifest into the verify primitive without re-encoding.

Test plan

  • cargo test --features metal,accelerate airc::realtime — 38 tests pass
  • CI build + clippy ratchet

🤖 Generated with Claude Code

Test and others added 2 commits May 25, 2026 22:02
…ubstrate)

Closes kanban card 290f64b7-5837-42ff-9844-570088fbb01a
Unblocks: L1-6 Phase B (peer_id -> pubkey lookup at envelope verify time)
Builds on: L1-4 #1446 (AircPeerManifest + capability index, just merged)

Why
- L1-6 #1448 shipped Phase A — ed25519 signing + 8-event contract chain
  envelope verify. Verify returns the pubkey that signed; the caller
  must cross-check it against an external trust source to confirm
  signer identity matches `proposer_id` / `bidder_id` / etc.
- That cross-check needs a peer_id -> pubkey directory. L1-4 #1446
  landed AircPeerManifest (peer_id, capabilities, room_ids, timestamps)
  but no pubkey. Without the pubkey, Phase B verify can't bind a
  signed envelope to a manifest-advertised peer identity.
- The substrate answer is "the manifest IS the trust directory" — no
  separate keyring, no out-of-band cert exchange. This commit
  completes that surface.

What this lands
- AircPeerManifest grows a required `signing_pubkey_hex: String` field.
  32-byte ed25519 public key, hex-encoded (64 chars, no 0x prefix).
  Matches `SignedContractEvent::signer_pubkey_hex` byte-for-byte —
  same encoding, no transcoding when L1-6 Phase B parses one for verify.
- AircPeerManifest::validate() validates the field structurally (length
  + hex chars). Curve-membership / point-on-line validation is
  delegated to ed25519_dalek when a consumer parses the bytes.
- AircPeerManifestError enum (EmptyPeerId / PubkeyWrongLength /
  PubkeyNonHexChar) — specific variants so the inbound L1-2 subscriber
  can log + reject with actionable diagnostics rather than a generic
  "bad manifest". Per the never-swallow-evidence rule.
- ts-rs auto-export updates shared/generated/airc/AircPeerManifest.ts
  with the new field as required `signingPubkeyHex: string`.
- Doc comment on the type explains the trust-directory rationale +
  the key-rotation answer (mutated pubkey for same peer_id = reject;
  rotation goes through a separate trust-rotation event class, not
  silent overwrite).

Tests (6 new + all 38 realtime tests pass)
- validates_well_formed_pubkey
- accepts_uppercase_hex (substrate must NOT reject otherwise valid
  uppercase just for case)
- rejects_wrong_length_pubkey
- rejects_non_hex_pubkey
- rejects_empty_peer_id
- round_trips_through_json_with_pubkey (verifies camelCase wire form
  + serde round-trip)

Field naming: signing_pubkey_hex matches L1-6's `signer_pubkey_hex`
on the envelope side. The "signing" framing (vs "signer") emphasizes
this is the key USED to sign anything by this peer, not a per-event
signer ID.

Wire impact: AircPeerManifest is a new type (L1-4 just landed); no
existing manifest traffic to migrate. Required field is the right call
here — make it impossible to advertise without the pubkey from day one.

Generated TS bindings barrel (shared/generated/airc/index.ts) also
picks up 3 backfill entries (AircCapabilityIndexEntry, AircPeerCapability,
AircPeerManifest) that L1-4 #1446's generator pass missed. Harmless
drift fold-in.

Follow-up: L1-6 Phase B PR will add `ContractVerifyingKey::from_hex` or
similar so Phase B verify reads signing_pubkey_hex straight from the
manifest into the verify primitive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joelteply joelteply force-pushed the feat/airc-peer-manifest-signing-pubkey branch from 4c92e13 to bf4918f Compare May 26, 2026 03:05
@joelteply joelteply merged commit 1c79a07 into canary May 26, 2026
4 checks passed
@joelteply joelteply deleted the feat/airc-peer-manifest-signing-pubkey branch May 26, 2026 03:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant