feat(contract): per-party contract cosigner group (1 cosigner, 1 user)#49
feat(contract): per-party contract cosigner group (1 cosigner, 1 user)#49aruokhai wants to merge 1 commit into
Conversation
The multi-user contract path stood up a single shared cosigner actor keyed by the cooperative key V′ that held EVERY recipient's counter-share C_i behind an N-entry authorization roster. N parties therefore wrote into one actor's storage namespace, which violates the one-cosigner-one-user invariant and defeats per-user enclave rollback detection: a user cannot keep a coherent post-write checksum chain when other parties interleave writes into the same actor's state. Introduce a ContractCosignerGroup abstraction (contract_groups/<V′>) that binds a group of dedicated per-party cosigners and carries the cross-party onboarding context (the cosigner's canonical V′ key package + the eVTXO taptree params) that cannot belong to any single party. Each party now gets its OWN dedicated contract cosigner, keyed by cc_id = sha256(V′||recipient_vk), holding ONLY that party's C_i (single-recipient policy) behind a single-entry roster — so 1 cosigner ⇔ 1 id (its group key) ⇔ 1 user, each with an isolated write stream. The wire protocol is unchanged: clients still address the group by V′ and send their own claimed_share. The host fans a cooperative spend out to the spending party's cc_id (rest_api sign routing), and the addressed V′ actor becomes a thin group COORDINATOR that owns onboarding only and authenticates just the author. - contract/group.rs: ContractCosignerGroup, cc_id derivation, register_member, spend_route fan-out resolver, persistence; unit-tested for determinism + per-party isolation. - onboarding/evtxo.rs: creation builds the group + the author's dedicated cosigner + a coordinator policy instead of one shared V′ actor. - handlers/onboard.rs: onboarding reads context from the group record and mints the participant's own dedicated cosigner. - rest_api.rs: SignStep1/2 fan out (V′, claimed_share) → cc_id. https://claude.ai/code/session_011FeY2KHoRDKnXeAGqx8EzB
|
Closing without merging. The load-bearing invariant is The existing design is already the right shape: one Per-user rollback-checksum isolation is still worth doing, but needs a cleverer approach that keeps Generated by Claude Code |
Problem
The multi-user contract path stood up a single shared cosigner actor keyed by the cooperative key
V′. That one actor held every recipient's counter-shareC_iin a singlerecipient_sharesmap behind an N-entry authorization roster (group_auth_idx[V′]), and every party resolved to it viapolicy_owner_idx[V′] = V′.So N parties wrote into one actor's storage namespace. This violates the one-cosigner-one-user invariant the rest of the system holds (a normal wallet is
1 cosigner = 1 id = its group key = 1 user), and it defeats per-user enclave rollback detection: a user can't keep a coherent post-write checksum chain when other parties interleave writes into the same actor's state.Fix — a contract cosigner group
Introduce a first-class
ContractCosignerGroup(persisted undercontract_groups/<V′>) that binds a group of dedicated per-party cosigners and carries the cross-party onboarding context — the cosigner's canonicalV′key package and the eVTXO taptree params — which legitimately cannot belong to any single party.Each party now gets its own dedicated contract cosigner, keyed by
holding only that party's
C_i(a single-recipientEvtxoPolicy) behind a single-entry roster — so the global invariant holds for contracts too: 1 cosigner ⇔ 1 id (its group key) ⇔ 1 user, each with its own isolated write stream.Wire protocol is unchanged
Clients still address the group by
V′and send their ownclaimed_share. The change is entirely server-side:cc_id(rest_apiSignStep1/2 routing), so the spend lands on an isolated single-user actor.V′actor becomes a thin group coordinator that owns onboarding only and authenticates just the author (so it too is1 actor ⇔ 1 user).V′ == Vsingle-author eVTXOs (already single-user, never a group) are untouched —spend_routereturnsNoneand the route is unchanged.Changes
contract/group.rs(new)ContractCosignerGroup,cc_idderivation,register_member,spend_routefan-out resolver, persistence. Unit-tested for determinism + per-party isolation.onboarding/evtxo.rsV′actor.cosigner/handlers/onboard.rsrest_api.rsSignStep1/SignStep2fan out(V′, claimed_share) → cc_id.protocol/protos/mpc_wallet.protoTesting
cargo check(workspace) — clean.contract::grouppass:cc_iddeterminism + per-party uniqueness, andregister_memberisolation (each cosigner's policy holds only its own recipient share, single-entry roster, correct self-route + group fan-out).contract_gatetests still require the example WASM artifacts to be pre-built (they panic with "build the contract first" on a bare checkout — unrelated to this change; CI builds them).Notes / scope
Per the agreed scope this is the structural regrouping only. The per-write rollback checksum mechanism it enables (returning a monotonic checksum to each party so clients can detect host rollback) is a follow-up that builds on these now-isolated per-party write streams. End-to-end enclave verification of the new actor topology is recommended before merge.
https://claude.ai/code/session_011FeY2KHoRDKnXeAGqx8EzB
Generated by Claude Code