feat(airc): add signing_pubkey_hex to AircPeerManifest (L1-6 verify substrate)#1451
Merged
Merged
Conversation
…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>
4c92e13 to
bf4918f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the missing
signing_pubkey_hexfield toAircPeerManifestso the L1-6 contract event chain can dopeer_id → pubkeylookups at verify time. The substrate's trust answer becomes "the manifest IS the directory" — no separate keyring, no out-of-band cert exchange.290f64b7-5837-42ff-9844-570088fbb01aAircPeerManifest+ 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 matchesproposer_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, no0xprefix). MatchesSignedContractEvent::signer_pubkey_hexbyte-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.AircPeerManifestErrorenum (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.shared/generated/airc/AircPeerManifest.tswith the new field as requiredsigningPubkeyHex: string.Why required (not optional)
AircPeerManifestis 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_idreject 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_pubkeyaccepts_uppercase_hex— substrate must NOT reject otherwise valid uppercase just for caserejects_wrong_length_pubkeyrejects_non_hex_pubkeyrejects_empty_peer_idround_trips_through_json_with_pubkey— verifies camelCase wire form (signingPubkeyHex) + serde round-tripBonus fold-in
Generated TS barrel
shared/generated/airc/index.tspicks 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 readssigning_pubkey_hexstraight from the manifest into the verify primitive without re-encoding.Test plan
cargo test --features metal,accelerate airc::realtime— 38 tests pass🤖 Generated with Claude Code