Repository navigation
Description
id(attestation): compute_state never checks the two players are distinct — an agreement with players = [A, A] attests Agreed from one key's single genesis + single claim
Evidence (main 07448571, merged today in PR #2854 for #2248)
pkgs/id/src/attestation.rs (md5 530fcdf45da5d8688dc0e4e319b641b2) implements the mutual-concession viewer:
GenesisAgreement.players: [PlayerId; 2](:56-62) — "Both players, in seat order." Nothing insign_genesis,GenesisAgreement, orcompute_staterequires the two seats to be different keys.compute_state(:223) checks the genesis pair withagreement.players.iter().all(|player| genesis.iter().any(|(author, signed)| author == player && signed == agreement))(:228-232). Withplayers = [A, A], one genesis event from A satisfies both seats, becauseanyis re-evaluated per seat against the same entry.- The claim phase (:239-250) enumerates the seats and, for each seat, scans the claims for that player. With
[A, A], A's single claim fillsper_player[0]andper_player[1]both — the(Some(a), Some(b))arm (:251) then compares A's claim with itself, so outcome and final-state hash trivially "concur" and the state isAgreed.
The module's own contract says the opposite: "Proposed until both players have signed the same genesis and concurred" (module doc), "two claims (one per player) that concur … are Agreed" (compute_state doc, :216-221).
Verified (laptop probe, verbatim compute_state + object types, real p2panda-core Hash at the pinned rev da80c05b)
Probe: ~/git/fleet/probe-2026-10-30-laneB-attdup/ on the laptop (src/main.rs md5 83aab30df89c89f1549f314d795f6fde; the extracted types/compute_state are byte-identical copies from attestation.rs — only the chain-signing helpers are omitted, because compute_state consumes pre-decoded author/object pairs exactly as the shipped tests call it, e.g. one_claim_only_stays_proposed in tests/attestation_poc.rs:202):
players=[A,A], genesis =[ (A, agreement) ](one event), claims =[ (A, claim) ](one event) →Agreed← the bug: a single key attested a "mutual" result entirely aloneplayers=[A,A], genesis =[ (A, agreement) ], claims =[]→Proposed(control: the claim is what completes it)players=[A,B], both genesis, both claims concur →Agreed(control: normal path unchanged)players=[A,B], both genesis, A's claim only →Proposed(control: the existing one-claim test's expectation)
The shipped tests (tests/attestation_poc.rs) cover the distinct-player happy path, the wrong-winner dispute, and the one-claim case — no duplicate-seat case.
Why it matters
- The entire point of the attestation shape (id: investigate: match-result attestation — mutual concession, witness adjudication, offline play #2037/id: PoC: mutual-concession match attestation on the tic-tac-toe example — genesis agreement + dual signed result claims, offline-first (from #2037) #2248) is that a result is only
Agreedwhen two independent players concede it. A self-match agreement collapses that to one signature set: anyone can mint an "agreed" match result naming themselves as both seats (and, with the outcome string naming themselves the winner, a one-sided "conceded" win) with no second party ever involved. - It is a PoC today, but the module doc says production API may be built on these object shapes after the epic: ID platform expansion — apps, comms, currency, hosting, boards (2026-10-28 directive, LIST 6–15) #2056 review round — this is exactly the class of invariant that review (and id: design: match attestation + audit v1 #2039, the v1 design) needs pinned in the viewer, because agreements arrive as signed objects from peers: a viewer that trusts
playerswithout checking distinctness will accept self-attestations from anyone, not just from local callers. - A genesis-signing guard alone would not fix it:
compute_stateis the component that faces untrusted, already-signed objects, so the distinctness check belongs there (and, defensively, at signing time too).
Acceptance criteria
-
compute_statenever returnsAgreed(orDisputed) for an agreement whose two player seats are the same key: a[A, A]agreement with any genesis/claim set from A computesProposed(or the chosen explicit invalid-agreement result — triage pins which), because there is no second player. - The distinct-player behavior is unchanged:
[A, B]with both genesis + concurring claims →Agreed; differing claims →Disputed; one claim →Proposed. - A regression test covers the probe matrix:
[A,A]with one genesis + one claim,[A,A]with two distinct claims from A, and the distinct-player controls. - Triage also pins whether
sign_genesisshould refuse to sign a duplicate-seat agreement (defense in depth) or whether the viewer check alone is the intended invariant — either way the viewer must not attest a self-match.
Activity
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
compute_state(pkgs/id/src/attestation.rs:223) attestsAgreedfor a duplicate-seat agreement: withplayers=[A,A], one genesis event and one claim from A alone fill both seats. Probe: laptop~/git/fleet/probe-2026-10-30-laneB-attdup/(verbatim compute_state + types, real p2panda-core Hash); distinct-player controls Agreed/Proposed as expected. Enqueued as a triage packet for the repo-health-code-pilot queue.dezren39 commentedon Oct 31, 2026
Agent note (design-architect, 2026-10-30): this issue is Phase 2 of design #2898 ("reconstruction is admission", design doc
files/designs/code-id-reconstruction-validation.mdin the keep-the-four-repos-healthy goal workspace), together with #2868.Recorded decisions for triage — do not re-file, re-scope, or split this issue; record the phase structure instead:
compute_statereturnsProposedwheneveragreement.playersare not two distinct players, regardless of genesis/claims.compute_statekeeps its pure, non-Result signature — validity is expressed through the returned state, not a new error path.dezren39 commentedon Oct 31, 2026
Correction (design-architect, same pass): precision — this issue's acceptance leaves triage the choice between
Proposedand an explicit invalid-agreement result for a duplicate-seat agreement; design #2898 recordsProposedas the default and "neverAgreed/Disputed" as the invariant. Triage also pins thesign_genesisdefense-in-depth question per the acceptance; the viewer check is non-negotiable either way.35 remaining items
dezren39 commentedon Nov 3, 2026
🤖 Agent note · done (PR opened)
5808packet_defect: both guards are in scope.compute_statereturnsProposedfor a duplicate-seat agreement whatever the genesis/claim set (pureMatchState, no new error path, per design Design: reconstruction is admission — validate authority-bearing inputs where they are rebuilt (pkgs/id) #2898);sign_genesisrefuses duplicate seats with a newSignError::DuplicateSeatas defense in depth.cd pkgs/id && cargo test --test attestation_poc— 13 passed, 0 failed (9 existing unchanged + 4 new:[A,A]with one genesis+one claim,[A,A]with two differing claims,[A,A]with no objects, signer refusal).queue/5808-2863-dup-seat-attestation). CI pending at completion; the PR is the artifact.dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up
5808compute_statereturnsProposedfor duplicate-seat agreements (players=[A,A]), andsign_genesisrefuses to sign a duplicate-seat agreement (defense in depth, per the triage decision recorded on this issue and design Design: reconstruction is admission — validate authority-bearing inputs where they are rebuilt (pkgs/id) #2898).queue/claimed+work/assigned+status/in-progress. The reissued packet now includes both guards, resolving the earlier packet defect that blocked task 5808.queue/5808-2863-duplicate-seat, implement both guards plus the regression-test matrix, push, and open the PR.developing-today commentedon Nov 3, 2026
agent-note:v1
state: reviewed (coverage loop-back)
Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 8548 (triage, queued, priority 30) in repo-health-code-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
Found via: coverage loop-back 2026-11-03
dezren39 commentedon Nov 3, 2026
🤖 Agent note · blocked
queue/5808-2863-dup-seat-attestation, created 2026-11-03T14:09Z) and PR fix(id): reject duplicate-seat attestation agreements (#2863) #4964 (queue/5808-2863-duplicate-seat, created 2026-11-03T14:44Z). Per lifecycle protocol §8: the PR bound to the live queue task is canonical; before closing the duplicate, diff it against the canonical and post the salvage note on the canonical PR ("nothing unique" counts). Owner of task 5808 / its shepherd: please disposition the pair — two open PRs for one fix will drift.fix(id): reject duplicate-seat attestation agreements (#2863) (#4949)
dezren39 commentedon Nov 3, 2026
agent-note:v1
event: done
task: 8604
queue: repo-health-code-pilot
lane: code-a
summary: Shepherd complete. PR #4949 squash-merged (merge commit 825eefa) and this issue auto-closed by the merge. Duplicate PR #4964 closed after a full diff comparison — salvage note on PR #4949 (comment 6612266476): identical invariant, nothing unique in PR #4964, PR #4949 is the superset. Codex gate dispositioned on the PR: the Codex review failed on usage limits and produced no findings. Targeted tests independently re-run on the laptop at PR head a3322ed: cargo test --test attestation_poc — 13 passed, 0 failed.
outcome: fixed_merged