Skip to content

[L1-6] (Phase A) ed25519 signing + 8-event contract chain primitives#1448

Merged
joelteply merged 1 commit into
canaryfrom
feat/l1-6-contract-event-chain
May 26, 2026
Merged

[L1-6] (Phase A) ed25519 signing + 8-event contract chain primitives#1448
joelteply merged 1 commit into
canaryfrom
feat/l1-6-contract-event-chain

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

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:

  • Pulls signer pubkeys from L1-4's presence:peer-manifest capability index at verify time
  • Subscribes to L1-2's AircEventTransport.replay() for audit-reproducible chain verification

What this lands

Rust truth (continuum-core::contracts)

signing.rs — ed25519 primitives matching airc-protocol's pinned ed25519-dalek = "2".

  • ContractSigningKey / ContractVerifyingKey wrappers — 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 — same bytes regardless of build
  • verify() returns Err (NOT Ok(false)) — callers can't accidentally treat a failed verify as success

event_classes.rs — 8 contract event class names + 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 (10): keygen→sign→verify roundtrip; pubkey roundtrip-through-bytes; bad-signature/wrong-payload/cross-key/wrong-length fail loud; ed25519-determinism; canonical-hash-stable-across-field-order
  • event_classes (3 + 8 ts-rs exports): all-8-names-distinct; all-use-contract-prefix; declare-registers-all-eight (dogfoods the L1-1 registry)
  • envelope (8): sign-then-verify; relabeling-attack-fails; payload/signature/pubkey-mutation-fails; JSON-bit-exact round-trip; hex-helpers
  • chain_tests (3): full 8-event proposed → bid → accepted → executing → delivered → verified → paid worked example (zero-LP household tier "ping grid dispatch"); disputed event signs + verifies; JSON-bit-exact round trip on the full chain

Test plan

  • cd src/workers/continuum-core && cargo test --features metal,accelerate contracts (31 tests)
  • Cryptographer review of the canonical-hash + relabeling-attack-resistance design

🤖 Generated with Claude Code

…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>
@joelteply joelteply merged commit aef2c80 into canary May 26, 2026
4 checks passed
@joelteply joelteply deleted the feat/l1-6-contract-event-chain branch May 26, 2026 01:43
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>
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