Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/src/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
204 changes: 204 additions & 0 deletions docs/src/RESOLVER_GOVERNANCE.md
Original file line number Diff line number Diff line change
@@ -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 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
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.
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading