From 218dfbb35373f35a604411a94b99cf3d1e39e7f1 Mon Sep 17 00:00:00 2001 From: xtep103 <302179003+xtep103@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:22:47 +0100 Subject: [PATCH 1/2] docs: document resolver committee onboarding and offboarding process --- docs/src/DEPLOYMENT.md | 9 +- docs/src/RESOLVER_GOVERNANCE.md | 204 ++++++++++++++++++++++++++++++++ docs/src/SUMMARY.md | 1 + 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 docs/src/RESOLVER_GOVERNANCE.md diff --git a/docs/src/DEPLOYMENT.md b/docs/src/DEPLOYMENT.md index 9430364..640fa0a 100644 --- a/docs/src/DEPLOYMENT.md +++ b/docs/src/DEPLOYMENT.md @@ -18,7 +18,7 @@ can be changed after `initialize`: | `token` | Any SEP-41 token your users already hold. No swap step exists, so picking a token nobody has is a dead deployment. | | `bond_amount` | Size from the spam/griefing model in [BOND_SIZING.md](BOND_SIZING.md): start with the larger of the assertion-spam and bad-faith-dispute floors (`R_case / tolerated spam per challenge window`), add any target attacker-loss margin, check finalize reward economics, then keep the result within the affordability cap for the smallest assertion value you want to support. Also capped at `MAX_BOND_AMOUNT`, a contract-enforced ceiling well above any realistic bond size. It exists so the bond can never overflow `finalize`'s reward-multiply arithmetic (the binding constraint) or the token balance held across a dispute. | | `challenge_window_secs` | Long enough that people who'd actually catch a bad assertion have a realistic chance to see it and act. Short windows finalize faster but catch less. See [V1_MAINNET_PARAMETERS.md](V1_MAINNET_PARAMETERS.md) for sizing guidance by monitoring tier. | -| `resolvers` | Odd-length, non-zero, distinct, and at most 21 addresses. `initialize` rejects duplicates with `DuplicateResolvers`. Pick people who'll actually be reachable to vote within a reasonable time of a dispute; a slow resolver committee stalls every disputed assertion until it acts. See [V1_MAINNET_PARAMETERS.md](V1_MAINNET_PARAMETERS.md) for size and composition trade-offs. | +| `resolvers` | Odd-length, non-zero, distinct, and at most 21 addresses. `initialize` rejects duplicates with `DuplicateResolvers`. Pick people who'll actually be reachable to vote within a reasonable time of a dispute; a slow resolver committee stalls every disputed assertion until it acts. See [V1_MAINNET_PARAMETERS.md](V1_MAINNET_PARAMETERS.md) for size and composition trade-offs, and [RESOLVER_GOVERNANCE.md](RESOLVER_GOVERNANCE.md) for the onboarding process behind picking these addresses in the first place. | | `finalize_reward_bps` | Basis points (0–1000) of the bond paid to whoever calls `finalize`. `caller` must authorize the call unconditionally, even at 0. 0 means no reward: the full bond returns to the asserter. A non-zero value creates an economic incentive for prompt finalization at the cost of a small bond haircut the asserter accepts when posting. 100 bps (1 %) is a reasonable starting point; 1000 bps (10 %) is the maximum enforced by the contract. | ## Canonical testnet deployment @@ -101,6 +101,13 @@ switch. ### Rotating the resolver committee +This section covers the on-chain mechanics only: who is authorized to call what, +and what each call does. For the off-chain side, who actually becomes a resolver, +how their key is generated and secured, what they commit to, and which of the two +paths below to use for a given real-world reason (resignation, inactivity, a +compromised key, a conflict of interest), see +[RESOLVER_GOVERNANCE.md](RESOLVER_GOVERNANCE.md). + There are two paths. `update_resolvers` is the admin emergency override; it works whether paused or not, so a compromised committee can be replaced without waiting to unpause: diff --git a/docs/src/RESOLVER_GOVERNANCE.md b/docs/src/RESOLVER_GOVERNANCE.md new file mode 100644 index 0000000..7ee1b55 --- /dev/null +++ b/docs/src/RESOLVER_GOVERNANCE.md @@ -0,0 +1,204 @@ +# Resolver committee governance: onboarding and offboarding + +This is the human, off-chain half of running a resolver committee. See +[DEPLOYMENT.md](DEPLOYMENT.md#rotating-the-resolver-committee) for the on-chain +mechanics (`update_resolvers`, `propose_rotation`, `vote_rotation`) and +[ROTATION_DESIGN.md](ROTATION_DESIGN.md) for why self-rotation works the way it +does. Neither of those documents says who should hold a resolver seat, how that +person secures their signing key, what they're expected to do when a dispute +lands, or what actually happens the day a resolver needs to be replaced. This +document is that operational layer, for anyone running a real (non-toy) +deployment. + +None of this is enforced by the contract. `initialize`, `update_resolvers`, and +`propose_rotation` accept any distinct addresses; nothing on-chain checks who +controls them, how the key was generated, or whether the person behind it agreed +to anything. The guarantees below come entirely from the deployment operator +choosing to follow this process and documenting that they have. + +## Onboarding a new resolver + +### Selection criteria + +[V1_MAINNET_PARAMETERS.md](V1_MAINNET_PARAMETERS.md#part-3-resolver-committee-size-and-composition) +already establishes the two properties that matter most for anyone on the +committee: reachability within `challenge_window_secs`, and independence from +the other resolvers (no shared employer, custody provider, or coordination +channel that turns several addresses into one correlated point of failure). +Onboarding is where those properties get checked before a seat is handed out, +not after: + +- **Domain competence.** A resolver has to be able to actually judge the + claims this specific deployment adjudicates. What that means varies by + deployment: for a freelance-escrow use case (see `demos/freelance-escrow`) + it might mean familiarity with the kind of work being disputed; for an + oracle-style deployment it might mean the ability to verify a claimed + real-world outcome from public sources. Generic trustworthiness isn't a + substitute for being able to tell a good assertion from a bad one in the + deployment's actual domain. +- **Demonstrated reachability.** Ask for evidence, not a promise: a track + record of availability on a timescale compatible with `challenge_window_secs` + for this deployment. Someone who is highly qualified but unreachable within + the window is worse for the committee than someone less qualified who + reliably responds. +- **Independence from the existing committee and from likely parties.** Screen + for shared employer, shared custody provider, and shared off-chain + coordination (the same group chat, the same on-call rotation) with current + resolvers, per the composition guidance already linked above. Also screen + for relationships to parties likely to assert or dispute on this deployment; + see the conflict-of-interest section below for why this matters beyond what + the contract itself checks. +- **No conflicting role in the deployment.** A resolver shouldn't also be an + operator with an economic stake in outcomes (e.g. the deployer taking a cut + of disputed bonds) beyond the ordinary `finalize_reward_bps` mechanism, which + applies uniformly and isn't resolver-specific. + +### Key generation and custody + +The resolver's signing key is what `resolve`, `propose_rotation`, and +`vote_rotation` authenticate against, so its custody model should match what +the resolver seat is actually worth to an attacker: the value of the disputes +it's likely to swing, not the deployment's total value locked. + +- **Plain single key (`stellar keys generate`).** What the canonical testnet + deployment uses today, and reasonable for a low-value or clearly-labeled + testnet/staging deployment where the committee itself is a stopgap (see + [DEPLOYMENT.md](DEPLOYMENT.md#canonical-testnet-deployment)'s own caveat + about `resolver1`/`resolver2`/`resolver3`). Fastest to set up, weakest + guarantee: a single lost or leaked key is a fully compromised resolver seat + with no recourse except rotating them out. +- **Hardware wallet.** Recommended default once a deployment has any real + value at stake. The resolver holds one address's worth of signing power on + a device that never exposes the private key to a networked machine. + Meaningfully raises the bar against remote compromise without adding + operational complexity for the resolver's day-to-day `resolve` calls, and + doesn't require coordinating with anyone else to vote. +- **Multisig behind the resolver's address.** Appropriate when a resolver + seat represents an organization rather than an individual (e.g. a curated + public list, or the "broad, visible representation" case + [V1_MAINNET_PARAMETERS.md](V1_MAINNET_PARAMETERS.md#part-3-resolver-committee-size-and-composition) + describes for 7+-member committees), or when a single resolver's vote + carries enough weight that no one person should be able to cast it + unilaterally. Adds coordination overhead to every single vote, which cuts + against reachability within the challenge window; weigh that cost against + the deployment's actual value at stake before choosing this over a hardware + wallet. + +Whichever model is chosen, the resolver (not the deployment operator) should +control the key material. The operator's role is validating that a reasonable +custody model is in place before proposing the address for the committee, not +holding the key on the resolver's behalf. + +### Commitments a resolver makes + +Before an address is proposed for `initialize`, `update_resolvers`, or +`propose_rotation`, the person behind it should explicitly agree to: + +- **A response-time commitment compatible with `challenge_window_secs`.** + State it in concrete terms (e.g. "will vote on an open dispute within N + hours of being notified") rather than "will be reachable," so both sides can + later judge objectively whether it was kept. This is the off-chain + counterpart to the reachability criterion above, made auditable. +- **Conflict-of-interest disclosure, both at onboarding and ongoing.** `resolve` + rejects a vote where the resolver is also the assertion's `asserter` or + `disputer` (`SelfVote`, added in #165) — but that check is necessarily + narrow: it only catches the resolver being a direct party to the specific + dispute in front of them. It cannot catch a resolver with an undisclosed + financial or personal relationship to one of the parties, since the + contract has no way to know about relationships that don't show up as an + on-chain address match. A resolver should disclose any such relationship to + the rest of the committee (and recuse informally by voting neither way) the + moment they become aware a specific dispute implicates it. This disclosure + is not enforced by the contract and depends entirely on the resolver's own + good faith. +- **Notice before going dark.** A resolver who knows they'll be unreachable + for an extended period (travel, planned unavailability) should say so ahead + of time, so the committee can decide whether to route around it or start a + replacement conversation before a dispute actually stalls. + +None of these commitments are enforceable by the contract; they're operating +norms the deployment should document and hold resolvers to. Keeping a record +of who agreed to what (and when) is what makes a later removal-for-cause +defensible instead of an admin's unilateral judgment call. + +## Offboarding and replacement + +### What triggers a replacement + +- **Inactivity.** A resolver who repeatedly misses their response-time + commitment, or who has gone dark without notice. Note the distinction from + the contract's own liveness fallback: `reclaim_stalled_dispute` (when a + deployment has `set_stall_timeout` configured) unwinds a single stalled + dispute by returning both bonds with no winner — it resolves that one case, + it does not remove the unresponsive resolver from the committee. Persistent + inactivity is a reason to replace the resolver, not just a reason to reclaim + the dispute they stalled. +- **Compromised key.** Any signal that a resolver's signing key may have + leaked or been used without their authorization. Treat this as urgent: see + the admin-override guidance below. +- **Conflict of interest.** A disclosed (or discovered) relationship to a + party that makes continued service on the committee inappropriate, whether + or not any specific vote was actually affected. +- **Resignation.** A resolver who no longer wants or is able to serve. + +### Who initiates it + +Either the affected resolver themselves (resignation, disclosed conflict, +planned unavailability) or another committee member who observes a problem +(inactivity, a suspected compromise, an undisclosed conflict they've learned +of). The deployment operator (the admin key holder) can also initiate a +replacement, but for anything short of a compromised key or a stalled +committee, initiating through the committee's own self-rotation path is +preferable; see the choice of path below. + +### Choosing self-rotation vs. the admin override + +Both paths write the same committee; the choice is about who should be making +the decision and how urgently it needs to happen, not a technical constraint +(both are documented in full in +[DEPLOYMENT.md](DEPLOYMENT.md#rotating-the-resolver-committee)): + +- **Use self-rotation (`propose_rotation` / `vote_rotation`) for the routine + cases**: resignation, a disclosed conflict of interest, planned + unavailability, or inactivity the rest of the committee can still act on + without the outgoing resolver's cooperation. This keeps the decision inside + the committee, requires no admin involvement, and is the expected path for + anything that isn't an emergency. A resolver proposes their own removal (or + another resolver proposes it, naming the affected seat), and the rest vote. +- **Use the admin override (`update_resolvers`) only for the emergency + cases**: a compromised key that needs to be cut off immediately, or a + committee that has lost the ability to reach majority at all (more than + `(n-1)/2` resolvers simultaneously unreachable, uncooperative, or otherwise + unable to vote). This is exactly the deadlock scenario + `docs/src/ROTATION_DESIGN.md` calls out: a compromised or deadlocked + committee cannot be expected to self-heal by vote, which is why the admin + path exists at all and stays pause-exempt. Using it for a routine, + uncontested resignation is a bad default even though it's technically + capable of doing the same thing — it reintroduces the single-admin-key + trust path that self-rotation exists to avoid, for a case that didn't need + it. + +A real deployment needs to decide, in advance, who holds the admin key that +can exercise this override and under what conditions they're expected to use +it (per +[V1_MAINNET_PARAMETERS.md](V1_MAINNET_PARAMETERS.md#part-3-resolver-committee-size-and-composition)'s +own framing) — not discover the answer for the first time during an incident. + +### Practical notes on executing a replacement + +- A rotation only ever affects disputes opened after it executes; the + outgoing resolver keeps their vote on any dispute already snapshotted + against the old committee (see `update_resolvers` in + [CONTRACT.md](CONTRACT.md) and `docs/src/ROTATION_DESIGN.md`'s section on + the per-dispute snapshot). If a resolver is being removed for a compromised + key or an active conflict, check whether they're a snapshotted voter on any + currently `Disputed` assertion; removing them from the live committee does + not retroactively remove their vote there. +- Confirm the incoming resolver's key and custody model (per the onboarding + section above) before the rotation executes, not after — once + `vote_rotation` reaches majority, the swap is immediate. +- Document the reason for every replacement (which of the four triggers + above applied) alongside the on-chain `RotationExecuted` / + `ResolversUpdated` event. The event proves *that* the committee changed; + it doesn't record *why*, and that reasoning is what makes the deployment's + dispute history auditable later. diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index d4ea30a..6755332 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -11,6 +11,7 @@ - [Deployment & Operations](DEPLOYMENT.md) - [Bond Sizing Analysis](BOND_SIZING.md) - [V1 Mainnet Parameter Selection](V1_MAINNET_PARAMETERS.md) + - [Resolver Committee Governance](RESOLVER_GOVERNANCE.md) - [Deployment & Operations: Protocol v2](DEPLOYMENT_V2.md) - [V2 Bond Sizing and Window Analysis](V2_BOND_SIZING.md) - [Integration Guide](INTEGRATION.md) From ae1363860b136847711c5f1955b2390a65e802e0 Mon Sep 17 00:00:00 2001 From: Ezedike-egwom Collins Date: Mon, 7 Sep 2026 18:49:59 +0100 Subject: [PATCH 2/2] docs: correct SelfVote attribution to PR 203 --- docs/src/RESOLVER_GOVERNANCE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/RESOLVER_GOVERNANCE.md b/docs/src/RESOLVER_GOVERNANCE.md index 7ee1b55..37f0936 100644 --- a/docs/src/RESOLVER_GOVERNANCE.md +++ b/docs/src/RESOLVER_GOVERNANCE.md @@ -101,8 +101,8 @@ Before an address is proposed for `initialize`, `update_resolvers`, or counterpart to the reachability criterion above, made auditable. - **Conflict-of-interest disclosure, both at onboarding and ongoing.** `resolve` rejects a vote where the resolver is also the assertion's `asserter` or - `disputer` (`SelfVote`, added in #165) — but that check is necessarily - narrow: it only catches the resolver being a direct party to the specific + `disputer` (`SelfVote`, added in PR #203, closing #165). That check is necessarily + narrow. It only catches the resolver being a direct party to the specific dispute in front of them. It cannot catch a resolver with an undisclosed financial or personal relationship to one of the parties, since the contract has no way to know about relationships that don't show up as an