[L1-6] (Phase A) ed25519 signing + 8-event contract chain primitives#1448
Merged
Conversation
…n primitives Roadmap item L1-6 — Phase A. Builds on L1-1 (#1445) for the event-class registry. Phase B (verify-on-replay via L1-4's peer-manifest + airc-cursor replay over L1-2 transport) lands in a follow-up once L1-4 merges. Closes roadmap item L1-6 (Phase A — primitives + types + registration + tests) Depends on: L1-1 (PR #1445, pending review) Defers: L1-4 (presence:peer-manifest, in flight by claude-tab-1) + L1-2 (AircEventTransport trait, already merged as #1443) Spec: GRID-BUS-ARCHITECTURE §4.4 + MULTI-PEER-COMMANDS §7 Why split Phase A vs B - Phase A is pure crypto + types + declarations — zero runtime deps on L1-4 or L1-2 transports. - Phase B wires the verifier-side: pulls signer pubkeys from L1-4's peer-manifest index, hooks into L1-2's AircEventTransport.replay() for audit-replayable chain verification. - Shipping A now means review can focus on the cryptographic substance before transport plumbing layers on top. What this lands Rust truth (continuum-core::contracts): - signing.rs — ed25519 primitives matching airc-protocol's pinned ed25519-dalek = "2". Wrappers ContractSigningKey + ContractVerifyingKey give future migration room (HSM, secure enclave) without touching call sites. Deterministic ed25519 → replay-equivalent signatures across peers. canonical_hash() uses serde_json's BTreeMap-backed Value for key-sorted SHA-256 input — same bytes regardless of build, the keystone for cross-peer verify-equality. Verify returns Err on failure (NOT Ok(false)) so callers can't accidentally treat a failed verify as success. - event_classes.rs — the 8 contract event class names (constants) + typed payload structs (ts-rs export to shared/generated/contracts/). Each payload carries contract_id for chain correlation. declare_contract_event_classes() registers all 8 with the L1-1 registry, broadcast=true, channel=Global, schemaVersion=v1. - envelope.rs — generic SignedContractEvent<P> wrapper. Signature pins (event_name, payload) together so relabeling attacks (presenting a bid sig as proposed) fail verification. Hex-encoded pubkey + signature on the wire. Tests (31 pass via cargo test --features metal,accelerate contracts) - signing: keygen→sign→verify roundtrip, pubkey roundtrip-through-bytes, bad-signature-fail-loud, wrong-payload-fail-loud, cross-key-verify-fail, ed25519-determinism, canonical-hash-stable-across-field-order, signature/pubkey length validation. - event_classes: all-8-names-distinct, all-use-contract-prefix, declare-registers-all-eight (dogfoods the L1-1 registry). - envelope: sign-then-verify roundtrip, relabeling-attack-fails, payload-mutation-fails, signature-mutation-fails, pubkey-swap-fails, JSON-round-trips-bit-exact, hex-helpers roundtrip + reject-bad-input. - chain_tests: full 8-event proposed→bid→accepted→executing→delivered→ verified→paid worked example (zero-LP household tier "ping grid dispatch"), disputed-event-signs-and-verifies, JSON-bit-exact round trip on the full chain. What this does NOT do (Phase B follow-up) - Pull signer pubkeys from L1-4's presence:peer-manifest index at verify time. Today verify returns the pubkey-that-signed; callers must cross-check against an external trust source. - Subscribe to airc-cursor replay over L1-2's AircEventTransport for audit-reproducible chain verification. - TS thin SDK wrapper (parallel to @system/events/shared/EventClass.ts). Deferred until a TS consumer materializes — Phase A consumers are Rust-side (router daemon, persona admission). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
joelteply
pushed a commit
that referenced
this pull request
May 26, 2026
…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
added a commit
that referenced
this pull request
May 26, 2026
…ubstrate) (#1451) * feat(airc): add signing_pubkey_hex to AircPeerManifest (L1-6 verify substrate) 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> * fix(airc): rebase peer manifest pubkey on uuid rooms --------- Co-authored-by: Test <test@test.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
L1-6 Phase A — pure Rust primitives. Phase B (verify-on-replay using L1-4's peer-manifest + audit-replay over L1-2's transport) is now unblocked since L1-4 (#1446) merged today and lands as a follow-up.
Why Phase A / Phase B split
Phase A is pure crypto + types + declarations. Zero runtime deps on the airc transport. Shipping it now means review can focus on the cryptographic substance before transport plumbing layers on top.
Phase B (next PR) wires:
presence:peer-manifestcapability index at verify timeAircEventTransport.replay()for audit-reproducible chain verificationWhat this lands
Rust truth (
continuum-core::contracts)signing.rs— ed25519 primitives matching airc-protocol's pinneded25519-dalek = "2".ContractSigningKey/ContractVerifyingKeywrappers — future migration room (HSM, secure enclave) without touching call sitescanonical_hash()usesserde_json'sBTreeMap-backedValuefor key-sorted SHA-256 — same bytes regardless of buildverify()returnsErr(NOTOk(false)) — callers can't accidentally treat a failed verify as successevent_classes.rs— 8 contract event class names + typed payload structs (ts-rs export toshared/generated/contracts/)contract_idfor chain correlationdeclare_contract_event_classes()registers all 8 with the L1-1 registry —broadcast: true,channel: Global,schemaVersion: v1envelope.rs— genericSignedContractEvent<P>wrapper(event_name, payload)together so relabeling attacks (presenting a bid sig as proposed) fail verificationTests (31 pass via
cargo test --features metal,accelerate contracts)proposed → bid → accepted → executing → delivered → verified → paidworked example (zero-LP household tier "ping grid dispatch"); disputed event signs + verifies; JSON-bit-exact round trip on the full chainTest plan
cd src/workers/continuum-core && cargo test --features metal,accelerate contracts(31 tests)🤖 Generated with Claude Code