Skip to content

SR-4 WI-1: CCI-keyed L2PS membership binding via Storage Programs #931

Description

@linear

What

Make L2PS subnet membership map to CCI primary claims via an anchored binding proof. Today L2PS identifies members by RSA keys; DACS-3 needs members identified by their CCI ClaimReference (CH-1, CH-3 of §8.3.1).

Why

Until L2PS is natively CCI-keyed, DACS-3 §8.3.2 specifies an interim binding-proof anchored via SR-2 (Storage Programs). Without it, in-channel signatures prove nothing about on-chain identity, and negotiate-rfq / negotiate-sealed-envelope cannot satisfy the conformance bar.

Scope (in sdks/src/l2ps/binding/)

type L2PSMembershipBinding = {
    bindingVersion: "1"
    channelId: string            // the L2PS subnet / session channel id
    subnetMemberId: string       // the member's L2PS/RSA identity within the subnet
    cciPrimaryClaim: ClaimReference  // from WI-0
    boundAt: number              // unix ms
    signature: string            // CCI-primary-key sig over canonical(binding minus signature)
}

// Public API:
export async function createMembershipBinding(
    channelId: string,
    subnetMemberId: string,
    claim: ClaimReference,
    demos: Demos,
): Promise<L2PSMembershipBinding>

export async function anchorMembershipBinding(
    binding: L2PSMembershipBinding,
    demos: Demos,
): Promise<{ storageProgramId: string }>   // SR-2 anchor

export async function resolveMember(
    channelId: string,
    subnetMemberId: string,
    demos: Demos,
): Promise<ClaimReference | null>   // null if no valid binding

export function verifyMembershipBinding(b: L2PSMembershipBinding): boolean

Signing rule

signed_bytes := "dacs-binding:v1:" || sha256(canonical_JCS(binding_without_signature)) — signed with signWithPrimaryClaim from WI-0. Reject any binding whose signature was produced by the RSA subnet key (the whole point of this work item).

Anchoring

  • Each binding lives in its own Storage Program OR a shared SP keyed by (channelId, subnetMemberId). Pick the simpler one — the brief leaves layout to us.
  • Anchor MUST happen before negotiation starts. resolveMember reads the anchored binding back via the SR-2 chain query.
  • Membership is fixed for channel lifetime (CH-1). No mutation API.

Acceptance

  • For an N-member subnet, each member has an anchored binding signed by their Demos key (not RSA subnet key).
  • resolveMember(channelId, subnetMemberId) returns the bound ClaimReference; returns null for unknown member.
  • A third party can verify subnet member ↔ CCI primary claim ↔ DACS-1 bundle for every member (DACS-1 bundle verification stays out of scope — we just expose the ClaimReference).
  • A message from an subnetMemberId without a valid binding is rejected by the WI-2 envelope verifier (test added in WI-2; cross-link).
  • Negative tests: (a) binding signed by RSA key → rejected; (b) tampered bound payload → signature fails; (c) replay of binding from one channelId into another → channelId mismatch rejection.

Dependencies

  • Blocked by WI-0 (ClaimReference + signWithPrimaryClaim).
  • Consumes existing @kynesyslabs/demosdk/storage for the SR-2 anchor.

Source

Brief §2 WI-1 + DACS-3 §8.3.2.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions