Skip to content

Proposal Workflow and Challenge System

RunTimeAdmin edited this page Oct 27, 2026 · 7 revisions

Proposal Workflow and Challenge System

**Referenced Files in This Document** - [SigvaraReputation.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/SigvaraReputation.sol) - [reputation-model.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/reputation-model.md) - [SigvaraReputation.t.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/test/SigvaraReputation.t.sol) - [index.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/index.js) - [chain.test.js](https://github.com/RunTimeAdmin/Sigvara/blob/main/oracle/chain.test.js) - [SigvaraOracleBond.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/SigvaraOracleBond.sol) - [InflowGuard.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/InflowGuard.sol) - [TimelockedGrants.sol](https://github.com/RunTimeAdmin/Sigvara/blob/main/src/TimelockedGrants.sol) - [0004-operator-custody.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/adr/0004-operator-custody.md) - [0005-governance-and-emergency-powers.md](https://github.com/RunTimeAdmin/Sigvara/blob/main/docs/adr/0005-governance-and-emergency-powers.md)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. Conclusion

Introduction

This document explains the optimistic reputation proposal workflow: an oracle proposes a score, a challenge window opens, a slashing committee can reject bad proposals, and anyone can finalize once the window closes. It also documents the backup operator flow through proposeIfEmpty, the security model around bonding and scorability checks, and the finalization process that transitions a pending score into a live, matured reputation.

The system is intentionally permissionless for execution: oracles propose, the committee rejects, and anyone can finalize. This avoids liveness dependence on any single actor while keeping accountability through roles, bonds, and time-bound challenges.

Project Structure

At the center of this workflow is the SigvaraReputation contract. The oracle network computes scores off-chain and calls into it; consumers read matured scores to gate trust. Tests and documentation describe the intended lifecycle, attack surface, and scoring model.

graph TB
Oracle["Oracle Network<br/>Computes scores"] --> Reputation["SigvaraReputation<br/>Propose / Finalize / Reject"]
Committee["Slashing Committee<br/>Rejects bad proposals"] --> Reputation
Consumer["On-chain Consumers<br/>Trust checks"] --> Reputation
Identity["SigvaraIdentity<br/>Agent registry"] --> Reputation
StakeView["Stake View<br/>Minimum stake check"] --> Identity
Loading

Diagram sources

Section sources

Core Components

  • Optimistic scoring model: oracles propose scores that are not immediately live. A time-bound challenge window allows rejection before finalization.
  • Roles:
    • ORACLE_ROLE: authorized to propose scores. When operatorBond is set, the caller must also be an active operator in it, or the signer of one.
    • SLASHING_COMMITTEE_ROLE: can reject a pending proposal during its challenge window.
    • STAKING_CORE_ROLE: can zero reputation after a slash.
    • UPGRADER_ROLE and admin roles manage upgrades and configuration. On main UPGRADER_ROLE also grants STAKING_CORE_ROLE and SLASHING_COMMITTEE_ROLE, while ORACLE_ROLE stays with the admin.
    • GUARDIAN_ROLE (main): can pause proposeReputation and proposeIfEmpty for three days at a time, and nothing else.
  • Data structures:
    • ReputationData: six-factor score plus last update timestamp.
    • PendingScore: proposed data, proposal timestamp, existence flag, evidence root, the proposer, and finalizableAt, the end of the review window fixed at proposal time.
  • Maturity: finalized scores become spendable gradually at a configured rate per day; drops apply immediately.

Key responsibilities:

  • Validate factor caps.
  • Enforce agent registration, non-slashed status, and minimum stake.
  • Open a challenge window on proposal, and fix its end then.
  • Allow committee rejection inside the window.
  • Allow permissionless finalization after the window, while the proposer could still make the proposal.
  • Track evidence roots for third-party verification.

The proposer, window and pause behaviour is live on the Arc testnet proxy since 26 October 2026. A proposal made before then stores no proposer and no finalizableAt and is judged against the live challengeWindow; the two that were pending at the upgrade have finalized.

Section sources

Architecture Overview

The lifecycle flows from proposal through challenge to finalization. The oracle writes a pending score; the committee may reject it; otherwise, anyone can finalize it after the window elapses.

sequenceDiagram
participant Oracle as "Oracle"
participant Rep as "SigvaraReputation"
participant Committee as "Slashing Committee"
participant Finalizer as "Anyone"
participant Identity as "SigvaraIdentity"
Oracle->>Rep : proposeReputation(didHash, data, evidenceRoot)
Rep->>Identity : getIdentity(didHash)
Rep->>Rep : validate factors and bond checks
Rep-->>Oracle : ScoreProposed event
Note over Rep : Challenge window open
alt Committee rejects
Committee->>Rep : rejectReputation(didHash)
Rep->>Rep : verify window active
Rep-->>Committee : delete pending proposal
Rep-->>Committee : ScoreRejected event
end
alt Window expires without rejection
Finalizer->>Rep : finalizeReputation(didHash)
Rep->>Rep : re-check scorability
Rep->>Rep : check the proposer is still authorized
Rep->>Rep : write live reputation and evidence root
Rep-->>Finalizer : ReputationUpdated event
end
Loading

Diagram sources

Section sources

Detailed Component Analysis

Lifecycle: proposeReputation → challenge window → finalizeReputation

  • proposeReputation:
    • Requires ORACLE_ROLE, and reverts while a guardian pause is running (main).
    • Validates each factor against its maximum.
    • Checks that the agent is registered, not slashed, and bonded via the identity registry’s stake view.
    • When operatorBond is set, checks that the caller is an active operator in it, or the signer of one.
    • Writes a PendingScore with current block timestamp, evidence root, the proposer’s address and finalizableAt.
    • Emits ScoreProposed.
  • Challenge window:
    • Defined by challengeWindow (held between 1 hour and 7 days), and fixed when the proposal is made: finalizableAt is proposedAt + challengeWindow as it stood then, so a later change moves no proposal already pending. A proposal written before the field existed reads zero and is judged against the live window.
    • During the window, the committee may call rejectReputation.
  • finalizeReputation:
    • Permissionless.
    • Re-checks scorability (agent must still be valid).
    • Ensures a pending proposal exists and finalizableAt has passed.
    • Ensures the proposer could still make the proposal: it holds ORACLE_ROLE and, with an operator set, is still an active operator in it or the signer of one. Otherwise it reverts with ProposerNotAuthorized. A proposal written before the proposer was recorded reads address(0) and is treated as live.
    • Computes anchor from the previously spendable score, writes live reputation and evidence root, clears pending slot, and sets maturity anchors.
    • Emits ReputationUpdated.
flowchart TD
Start(["Start"]) --> Propose["proposeReputation()<br/>Validate role, factors,<br/>agent state, stake"]
Propose --> Pending["Write PendingScore<br/>Set proposedAt, evidenceRoot,<br/>proposer and finalizableAt"]
Pending --> Window{"Challenge window active?"}
Window --> |Yes| RejectPath["rejectReputation() allowed"]
Window --> |No| FinalizePath["finalizeReputation() allowed"]
RejectPath --> Rejected["Delete pending proposal<br/>Emit ScoreRejected"]
Rejected --> End(["End"])
FinalizePath --> CheckScorable["Re-check agent scorable"]
CheckScorable --> CheckProposer{"Proposer still authorized?"}
CheckProposer --> |Yes| WriteLive["Write live reputation<br/>Clear pending<br/>Set maturity anchors"]
CheckProposer --> |No| Stranded["Revert ProposerNotAuthorized<br/>A new proposal replaces it"]
WriteLive --> Updated["Emit ReputationUpdated"]
Updated --> End
Stranded --> End
Loading

Diagram sources

Section sources

Backup Operator Flow: proposeIfEmpty and Compare-and-Swap Semantics

proposeIfEmpty is designed for a checking operator covering a silent primary. It performs a compare-and-swap on the pending slot:

  • If a pending proposal already exists and its proposer is still authorized, it reverts with ScoreAlreadyPending, including the existing proposedAt so the loser can identify whose proposal won.
  • If the slot is empty, or the pending proposal’s proposer has lost its authority, it proceeds to _propose, writing the backup proposal and opening a new window. A proposal whose proposer no longer holds ORACLE_ROLE, or whose operator was removed or unbonded or whose signer was cleared, cannot be finalized while its proposer lacks that authority, so it holds the slot in name only. Without this a checker covering for a removed primary would be locked out by the very removal meant to stop it. The September contract that ran on the Arc testnet until 26 October 2026 reverted for any pending proposal.

This prevents a checker from accidentally replacing a live primary proposal, which would restart the challenge window and erase divergence evidence.

flowchart TD
Start(["proposeIfEmpty Entry"]) --> ReadSlot["Read pendingScores[didHash]"]
ReadSlot --> Exists{"exists and proposer<br/>still authorized?"}
Exists --> |Yes| RevertRace["Revert ScoreAlreadyPending<br/>Include existing proposedAt"]
Exists --> |No| Propose["Call _propose()<br/>Validate and write PendingScore"]
Propose --> Event["Emit ScoreProposed"]
RevertRace --> End(["End"])
Event --> End
Loading

Diagram sources

Section sources

Slashing Committee Role: rejectReputation

The slashing committee can reject a pending proposal during its challenge window:

  • Requires SLASHING_COMMITTEE_ROLE.
  • Verifies a pending proposal exists and the window is still active (block.timestamp < finalizableAt, the end fixed when the proposal was made).
  • Deletes the pending proposal without altering the agent’s finalized score.
  • Emits ScoreRejected with the caller address.

A rejection needs no further ruling: the committee’s decision is the ruling, with no separate dispute step. A slash differs on main, where the committee files it and the admin, not the committee, rules if the operator disputes.

flowchart TD
Start(["rejectReputation Entry"]) --> HasPending{"pending.exists?"}
HasPending --> |No| RevertNoPending["Revert NoScorePending"]
HasPending --> |Yes| CheckWindow{"block.timestamp < finalizableAt?"}
CheckWindow --> |No| RevertExpired["Revert ChallengeWindowExpired"]
CheckWindow --> |Yes| DeletePending["Delete pendingScores[didHash]"]
DeletePending --> Emit["Emit ScoreRejected(committee)"]
Emit --> End(["End"])
RevertNoPending --> End
RevertExpired --> End
Loading

Diagram sources

Section sources

Security Model: Scorability, Bonding, and Evidence

  • Scorability checks:
    • Agent must be registered and not slashed.
    • Must hold minimum stake via the identity registry’s stake view.
  • Optional bonded operator requirement:
    • When operatorBond is set, proposers must be active operators in the bonded operator set, or the signer an active operator has named. The signing key of an oracle service need not be the key that owns the bond.
    • Unset means the check is disabled; this is intentional for single-operator setups. On main, setOperatorBond requires UPGRADER_ROLE, because passing zero switches the check off.
  • Proposer authority at finalization:
    • The proposer is stored with the proposal and asked about again when it is finalized, live and not as a snapshot, so revoking a key takes effect at once. Removing an operator, clearing its signer or revoking ORACLE_ROLE ends whatever that key has pending, without the committee rejecting each proposal in time.
    • The price is that a departing operator’s pending proposals are stranded until a successor replaces them. The alternative is a removed or compromised key’s proposals going live unreviewed.
  • Evidence roots:
    • Each proposal commits to a Merkle root of evidence used to compute the score.
    • Third parties can verify individual leaves against the stored root.

These checks ensure that only accountable, bonded agents receive reputation and that proposals carry verifiable evidence.

classDiagram
class SigvaraReputation {
+proposeReputation(didHash, data, evidenceRoot)
+proposeIfEmpty(didHash, data, evidenceRoot)
+finalizeReputation(didHash)
+rejectReputation(didHash)
+getTotalScore(didHash) uint8
+verifyEvidence(didHash, leaf, proof) bool
-_requireScorable(didHash)
-_requireBondedOracle()
-_proposerStillAuthorized(proposer) bool
}
class SigvaraIdentity {
+getIdentity(didHash) AgentIdentity
+stakeView() IStakeView
}
class IOperatorSet {
+isActiveOperator(address) bool
}
SigvaraReputation --> SigvaraIdentity : "reads agent state"
SigvaraReputation --> IOperatorSet : "optional bonded operator check"
Loading

Diagram sources

Section sources

Examples: Proposal Races, Challenges, and Finalization

Proposal Race Between Primary and Checker

  • Primary proposes first; checker later attempts proposeIfEmpty.
  • Expected outcome: checker reverts with ScoreAlreadyPending, preserving the primary’s proposal and its original challenge window.

Test reference:

Committee Rejects a Bad Proposal

  • Committee calls rejectReputation within the challenge window.
  • Expected outcome: pending proposal deleted; finalized score unchanged; ScoreRejected emitted.

Test references:

Finalization After Window Elapses

  • Anyone calls finalizeReputation after finalizableAt.
  • Expected outcome: pending cleared, live reputation written, maturity anchors set, ReputationUpdated emitted.

Test references:

Stranded Proposal After an Operator Is Removed

  • The operator that made a pending proposal is removed (or its signer cleared, or its ORACLE_ROLE revoked) before the proposal is finalized.
  • Expected outcome: finalizeReputation reverts with ProposerNotAuthorized and the score is not made live. A checking operator replaces the proposal with proposeIfEmpty at once, and a primary or another authorized operator replaces it with proposeReputation. The committee did not have to reject it inside its window.

Test references:

Section sources

Conceptual Overview

The optimistic model trades immediate finality for a bounded challenge period. Oracles propose; the community and committee can contest; finalizers execute once safe. Maturity ensures trust cannot be spent instantly, reducing farm-and-cash-out incentives.

stateDiagram-v2
[*] --> Proposed
Proposed --> Rejected : "committee rejects"
Proposed --> Finalized : "window expires"
Rejected --> Proposed : "new proposal"
Finalized --> [*]
Loading

Dependency Analysis

The reputation contract depends on:

  • Access control roles for proposing, rejecting, and zeroing.
  • TimelockedGrants and InflowGuard on main: the first lets the admin revoke upgrader-administered roles at once, the second adds the guardian’s pause on proposals.
  • Identity registry for agent state and stake view.
  • Optional bonded operator registry for proposer eligibility, asked again at finalization about the proposer.
  • Merkle proof utilities for evidence verification.
graph LR
Rep["SigvaraReputation"] --> AC["AccessControlUpgradeable"]
Rep --> UUPS["UUPSUpgradeable"]
Rep --> Identity["SigvaraIdentity"]
Rep --> Bond["IOperatorSet"]
Rep --> Merkle["MerkleProof"]
Loading

Diagram sources

Section sources

Performance Considerations

  • Factor validation occurs at proposal time; finalization reads and writes minimal storage.
  • Maturity accrues on read, allowing growth even if oracles stop proposing.
  • Evidence verification uses Merkle proofs, enabling efficient third-party audits without trusting oracle files.

Troubleshooting Guide

Common issues and their causes:

  • ScoreAlreadyPending:
    • A checker attempted to cover when a primary proposal was already live.
    • Action: inspect the returned proposedAt to identify the winning proposal; do not retry blindly.
  • ChallengeWindowActive:
    • Finalization attempted too early.
    • Action: wait until finalizableAt, which the error carries.
  • ChallengeWindowExpired:
    • Committee tried to reject after the window closed.
    • Action: use finalization instead; rejection is no longer possible.
  • NoScorePending:
    • Finalization or rejection called without a pending proposal.
    • Action: ensure a proposal exists and has not been rejected or finalized.
  • AgentNotRegistered / AgentSlashed / AgentNotBonded:
    • Scorability checks failed.
    • Action: confirm agent registration, non-slashed status, and minimum stake.
  • OracleNotBonded:
    • Proposer not an active bonded operator (or the signer of one) when operatorBond is set.
    • Action: ensure the oracle is admitted and bonded in the operator set, or that its operator has named it with setSigner.
  • ProposerNotAuthorized:
    • Finalization was attempted on a proposal whose proposer has lost ORACLE_ROLE, or is no longer an active operator or signer.
    • Action: do not retry; the proposal cannot be finalized while its proposer lacks that authority. A proposal from an authorized proposer replaces it.
  • InflowsPausedUntil:
    • A guardian pause is running.
    • Action: wait for the time in the error, or for the guardian or admin to lift it. Finalizing and rejecting proposals already made is not paused.
  • ChallengeWindowTooShort / ChallengeWindowTooLong / MaturityRateTooHigh:
    • The admin set a value outside its bounds: a window between 1 hour and 7 days, a maturity rate above 0 and at most 100 points a day.
    • Action: pick a value inside the bounds.

Operational notes:

  • Oracle index logs distinguish between losing a race (isSlotTaken) and other errors, helping operators avoid false alarms.
  • The primary treats ProposerNotAuthorized on finalize as a stranded proposal to replace, not as a failure. A checking operator asks the contract, with a dry run of proposeIfEmpty, whether an occupied slot is stranded, and covers for it if so; a question it cannot ask is answered “alive”.
  • Chain helpers expose getPendingScore and getChallengeWindow for off-chain monitoring.

Section sources

Conclusion

The proposal workflow centers on an optimistic scoring model with a bounded challenge window. Oracles propose scores backed by evidence; the slashing committee can reject bad proposals; and anyone can finalize once the window closes. proposeIfEmpty enables backup operators to cover gaps safely using compare-and-swap semantics, and it may replace a proposal whose proposer has lost its authority. Finalization stays permissionless, but a proposal is made live only while its proposer could still make it. Scorability and bonding checks ensure only accountable agents receive reputation, while maturity prevents instant spending of newly earned trust. Together, these mechanisms balance liveness, security, and economic accountability.

Overview

Clone this wiki locally