Skip to content

[Hackathon] harvard-phd: EigenTrust plugin with checkable invariants#2

Open
mariagorskikh wants to merge 2 commits into
mainfrom
hackathon/harvard-phd-eigentrust
Open

[Hackathon] harvard-phd: EigenTrust plugin with checkable invariants#2
mariagorskikh wants to merge 2 commits into
mainfrom
hackathon/harvard-phd-eigentrust

Conversation

@mariagorskikh

@mariagorskikh mariagorskikh commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Which piece

Trust layer. Adds eigentrust as a bundled alternative to the
default score_average plugin, registered under
(trust, eigentrust) in nest_core.plugins.

Why

The trust layer is the most interesting "what could go wrong"
surface in NEST: the bundled score_average is a running mean with
no Sybil resistance, no transitivity, no decay. The docs even point
to EigenTrust as "a good fit to test here". This PR delivers it —
not as an academic curiosity, but as a plugin whose correctness
properties are asserted in tests, not promised in comments.

Core idea

EigenTrust (Kamvar, Schlosser, Garcia-Molina, WWW 2003) computes the
global trust vector as the stationary distribution of a teleporting
random walk over the local-trust graph:

t = (1 - alpha) * C^T * t + alpha * p

where

  • C is the row-normalized local-trust matrix
    (c_ij = max(0, pos_ij - neg_ij) / row_sum, fallback to p when
    a row sums to zero — the "naive walker"), and
  • p is the seed distribution over pre-trusted peers (uniform
    fallback when no pre-trusted set is declared).

Solved by power iteration; pure Python, no numpy dependency, in
line with the rest of the reference plugin pack.

What's checkable, not just commented

The whole point of doing this in a verification-flavored way: every
property the algorithm claims is asserted in test_eigentrust.py.

Property Where it's enforced
Simplex (sum_i t_i == 1, t_i >= 0) _assert_on_simplex, hand-rolled + hypothesis
Row-stochasticity of C _assert_row_stochastic, hand-rolled + hypothesis
Fixed-point residual < tol _assert_fixed_point, hand-rolled + hypothesis
Sybil upper bound t_sybil <= alpha * p_sybil test_sybil_lower_bound
Weak monotonicity in honest positive evidence test_weak_monotonicity_of_positive_evidence
alpha = 1 recovers p exactly test_alpha_one_recovers_pretrusted_distribution
Pre-trusted mass lower bound t_i >= alpha * p_i property-based

Hypothesis is already a dev dependency, so the property tests cost
nothing to add.

How to test

# from the repo root, with the dev venv active
pip install -e packages/nest-core -e packages/nest-sdk -e packages/nest-plugins-reference
pip install pytest pytest-asyncio hypothesis

pytest packages/nest-plugins-reference/tests/test_eigentrust.py -v
# 16 tests, ~0.6s

Or wire it into a scenario:

# scenarios/reputation.yaml
layers:
  trust: eigentrust

The plugin is drop-in compatible with the existing Trust protocol,
so no scenario YAML changes are required to test it under the
reputation scenario or anywhere else.

Key assumptions

  • Threat model. The plugin records reports under the reporter's
    identity (the Evidence.reporter field). A Sybil cannot forge an
    honest agent's report; if NEST's auth/identity layers fail upstream,
    no trust layer can save you.
  • Pre-trusted set is optional. If you don't pass one, p is
    uniform and the Sybil-resistance bound degrades to the uniform
    case. The test for the Sybil upper bound makes this explicit by
    passing pretrusted=[a0].
  • No self-trust. Diagonal entries c_ii are forced to zero so
    agents can't short-circuit the random walk by endorsing
    themselves.
  • Pure Python. Deliberately no numpy: keeps the reference pack
    free of heavy deps. Power iteration runs in O(iters * |E|) over
    the local-trust graph; with DEFAULT_MAX_ITER=300 (sized for
    (1 - alpha)^k < tol) and the agent counts NEST actually
    simulates, this is sub-millisecond.

Persona

Harvard CS PhD candidate in formal methods — invariants beat
comments; a property you can check beats a promise that one holds.

Future work

  • A validate_reputation_sybil_bound validator that reads a trace
    and asserts the Sybil upper bound directly, on top of the existing
    reputation_scoring / reputation_warnings checks.
  • Time-decayed local trust (sliding window over evidence) — easy
    extension of the same fixed-point formulation.
  • TLA+ spec of the teleport mixture and refinement mapping to this
    Python implementation; left for a follow-up because a 1-PR
    hackathon is the wrong place for it.

https://claude.ai/code/session_01C5j2D4MgCkPgsjSCqBVpWW


Generated by Claude Code

Summary by Sourcery

Add an EigenTrust-based trust plugin as a bundled alternative to score_average, document it, and verify its core invariants with targeted and property-based tests.

New Features:

  • Introduce an EigenTrust trust plugin implementing transitive, Sybil-resistant reputation compatible with the existing Trust protocol.
  • Register the eigentrust plugin in the core plugin registry for use in scenarios via the trust layer configuration key.

Enhancements:

  • Expose introspection helpers on the EigenTrust plugin to inspect the global trust vector and row-normalized local trust matrix for validation and analysis.

Documentation:

  • Document the eigentrust trust layer, its probabilistic model, configuration, and how to enable it in scenarios.

Tests:

  • Add a comprehensive EigenTrust test suite covering protocol conformance, algorithmic invariants such as simplex, row-stochasticity, fixed-point convergence, and Sybil/monotonicity properties, plus property-based tests over random trust graphs.

EigenTrust (Kamvar et al., WWW 2003) computes global trust as the
stationary distribution of a teleporting random walk over the local
trust graph: t = (1 - alpha) * C^T * t + alpha * p. The reference
score_average plugin is Sybil-vulnerable and untransitive; EigenTrust
fixes both via a pre-trusted seed distribution and power iteration to
convergence.

What's checkable (not just commented):
  * simplex: sum_i t_i == 1, all t_i >= 0
  * row-stochasticity: every row of C sums to 1
  * fixed point: ||(1-a) C^T t + a p - t||_inf < tol
  * Sybil upper bound: t_sybil <= alpha * p_sybil for unendorsed Sybils
  * weak monotonicity in honest positive evidence

All five properties are asserted in test_eigentrust.py, including
hypothesis-driven property tests over randomly generated trust graphs.
Plugin is wired into the registry under (trust, eigentrust) so it can
be swapped into any scenario via `layers.trust: eigentrust`.
@sourcery-ai

sourcery-ai Bot commented May 26, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds an EigenTrust-based trust plugin as a first-class alternative to score_average, fully wired into the plugin registry, with a pure-Python implementation and an extensive invariant-focused test suite (unit + property-based) to validate its probabilistic, fixed-point, and Sybil-resistance properties.

Sequence diagram for scoring with the EigenTrust trust plugin

sequenceDiagram
    actor Scenario
    participant TrustLayer
    participant EigenTrust
    participant _compute_trust_vector
    participant _power_iterate

    Scenario->>TrustLayer: request_score(agent)
    TrustLayer->>EigenTrust: score(agent)
    EigenTrust->>EigenTrust: _agents.add(agent)
    EigenTrust->>_compute_trust_vector: _compute_trust_vector()
    _compute_trust_vector->>_compute_trust_vector: _pretrusted_distribution(agents)
    _compute_trust_vector->>_compute_trust_vector: _local_trust_raw(agents)
    _compute_trust_vector->>_compute_trust_vector: _normalize_local_trust(raw, agents, p)
    _compute_trust_vector->>_power_iterate: _power_iterate(c, p, alpha, agents, max_iter, tol)
    _power_iterate-->>_compute_trust_vector: t, iters, residual
    _compute_trust_vector->>EigenTrust: trust_vector t
    EigenTrust->>EigenTrust: set last_iters, last_residual
    EigenTrust-->>TrustLayer: ReputationScore
    TrustLayer-->>Scenario: ReputationScore
Loading

File-Level Changes

Change Details Files
Introduce EigenTrust trust plugin implementing EigenTrust algorithm with power iteration and invariant-friendly internals.
  • Implement EigenTrust class that conforms to the Trust protocol (score, report, attest, stake) while computing global trust via EigenTrust’s teleport-mixed stationary distribution.
  • Maintain per-reporter positive/negative evidence, construct raw local trust scores with no self-trust, normalize into a row-stochastic local trust matrix, and compute the trust vector via pure-Python power iteration with configurable alpha, max_iter, and tolerance.
  • Expose introspection helpers for the converged trust vector, local trust matrix, pretrusted distribution, and last convergence diagnostics to support testing and future validators.
packages/nest-plugins-reference/nest_plugins_reference/trust/eigentrust.py
Add comprehensive test suite asserting EigenTrust invariants, Sybil-resistance properties, and protocol conformance, including property-based tests.
  • Add conformance tests ensuring EigenTrust’s external behavior matches ScoreAverageTrust expectations: default uniform scores, effect of positive/negative reports, attestations, and stake no-op behavior.
  • Introduce helper assertion functions for simplex, row-stochasticity, and fixed-point residual, and use them in hand-crafted graph tests covering Sybil upper bound, monotonicity, and alpha=1 behavior.
  • Use Hypothesis strategies to generate random trust graphs and assert that simplex, row-stochasticity, fixed-point residual, and pretrusted mass lower bound properties hold under randomized evidence.
packages/nest-plugins-reference/tests/test_eigentrust.py
Register EigenTrust plugin and document its usage and properties in the trust layer docs.
  • Register the new EigenTrust implementation under the (trust, eigentrust) key in the central plugin registry so it can be selected in scenarios.
  • Extend trust layer documentation with a section describing the eigentrust plugin, its fixed-point formulation, checked properties, and how to enable it in scenario YAML.
  • Link to the new implementation and test files from the docs to make the plugin discoverable for users and reviewers.
docs/layers/trust.md
packages/nest-core/nest_core/plugins.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In report, silently overriding evidence.subject with the agent parameter when they differ could hide upstream bugs; consider either asserting they match or consistently using one source of truth so mismatches are surfaced early.
  • Both _normalize_local_trust and _power_iterate materialize dense rows over all agents for each reporter, which is fine for small scenarios but becomes O(n²) in memory/time; if you expect larger graphs it may be worth keeping C sparse by only storing non-zero entries and iterating over those.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `report`, silently overriding `evidence.subject` with the `agent` parameter when they differ could hide upstream bugs; consider either asserting they match or consistently using one source of truth so mismatches are surfaced early.
- Both `_normalize_local_trust` and `_power_iterate` materialize dense rows over all agents for each reporter, which is fine for small scenarios but becomes O(n²) in memory/time; if you expect larger graphs it may be worth keeping `C` sparse by only storing non-zero entries and iterating over those.

## Individual Comments

### Comment 1
<location path="packages/nest-plugins-reference/nest_plugins_reference/trust/eigentrust.py" line_range="375" />
<code_context>
+                    # No self-trust.  An agent rating itself would let it
+                    # short-circuit the random walk.
+                    continue
+                val = float(pos_row.get(j, 0) * _POS_WEIGHT - neg_row.get(j, 0) * _NEG_WEIGHT)
+                if val > 0.0:
+                    row[j] = val
</code_context>
<issue_to_address>
**issue (bug_risk):** Negative/positive weights are effectively applied twice, which may not match the intended semantics.

In `report()` each event is already scaled by `_POS_WEIGHT` / `_NEG_WEIGHT` when updating `_pos`/`_neg`. Multiplying by the same constants again here makes the effective weighting `weight ** 2` instead of `weight`. With both set to 1 this currently has no effect, but changing (say) `_NEG_WEIGHT = 3` would unintentionally give a 9x impact.

If the intent is to apply weighting only once when forming the raw local-trust score, either:
- increment by `1` in `report()` and keep the multiplication here, or
- keep the increments as they are and remove the extra multiplication here.
</issue_to_address>

### Comment 2
<location path="packages/nest-plugins-reference/nest_plugins_reference/trust/eigentrust.py" line_range="275-276" />
<code_context>
+        sample_count = sum(
+            len(self._pos[i]) + len(self._neg[i]) for i in self._agents
+        )
+        confidence = 0.0 if self._tol <= 0 else max(
+            0.0, min(1.0, 1.0 - self.last_residual / max(self._tol, 1e-12)),
+        )
+        return ReputationScore(
</code_context>
<issue_to_address>
**nitpick:** The confidence computation includes dead branches and redundant safeguards.

Because the constructor already enforces `tol > 0.0` (`if tol <= 0.0: raise ValueError`), both `self._tol <= 0` and `max(self._tol, 1e-12)` are unreachable/unused cases. You can simplify to:

```python
confidence = max(0.0, min(1.0, 1.0 - self.last_residual / self._tol))
```

This preserves behavior under the existing class invariant while making the confidence calculation more direct.
</issue_to_address>

### Comment 3
<location path="packages/nest-plugins-reference/tests/test_eigentrust.py" line_range="370" />
<code_context>
+    return agents, reports
+
+
+class TestEigenTrustProperties:
+    @settings(
+        max_examples=80,
</code_context>
<issue_to_address>
**suggestion (testing):** Consider varying the number of pretrusted agents in the property-based tests

Right now the property-based tests always use `pretrusted = [agents[0]]`, but the behavior of `p` and invariants like pretrusted mass lower bound and Sybil resistance depend on the structure of the pretrusted set. It would be useful to exercise several shapes of `pretrusted`.

You could, for example, extend `_trust_graph` (or add a small strategy) to draw `pretrusted` like:

```python
pretrusted = draw(
    st.lists(
        st.sampled_from(agents),
        min_size=1,
        max_size=min(3, len(agents)),
        unique=True,
    )
)
```

and reuse this in the property tests that reason about `p` to broaden coverage without much added complexity.

Suggested implementation:

```python
    for _ in range(edge_count):
        i = draw(st.integers(min_value=0, max_value=n - 1))
        j = draw(st.integers(min_value=0, max_value=n - 1))
        kind = draw(st.sampled_from(["positive", "negative"]))
        if i == j:
            continue
        reports.append((f"a{i}", f"a{j}", kind))

    pretrusted = draw(
        st.lists(
            st.sampled_from(agents),
            min_size=1,
            max_size=min(3, len(agents)),
            unique=True,
        )
    )

    return agents, reports, pretrusted

```

```python
    @settings(
        max_examples=80,
        deadline=None,
        suppress_health_check=[HealthCheck.function_scoped_fixture],
    )
    @given(_trust_graph())
    @pytest.mark.asyncio
    async def test_simplex_and_row_stochastic(
        self,
        graph: tuple[list[AgentId], list[tuple[str, str, str]], list[AgentId]],
    ) -> None:
        agents, reports, pretrusted = graph

```

1. Update all other usages of `_trust_graph()` in this file to unpack three values (`agents, reports, pretrusted = graph`) and fix any type hints accordingly.
2. In property tests that currently construct `pretrusted = [agents[0]]` (or similar), remove that construction and instead use the `pretrusted` drawn from `_trust_graph()` so that the tests exercise varying pretrusted sets.
3. If there are helper functions that accept `(agents, reports)` from `_trust_graph()`, update their signatures (and call sites) to also accept `pretrusted` or to accept a single `graph` tuple and unpack internally.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

# No self-trust. An agent rating itself would let it
# short-circuit the random walk.
continue
val = float(pos_row.get(j, 0) * _POS_WEIGHT - neg_row.get(j, 0) * _NEG_WEIGHT)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Negative/positive weights are effectively applied twice, which may not match the intended semantics.

In report() each event is already scaled by _POS_WEIGHT / _NEG_WEIGHT when updating _pos/_neg. Multiplying by the same constants again here makes the effective weighting weight ** 2 instead of weight. With both set to 1 this currently has no effect, but changing (say) _NEG_WEIGHT = 3 would unintentionally give a 9x impact.

If the intent is to apply weighting only once when forming the raw local-trust score, either:

  • increment by 1 in report() and keep the multiplication here, or
  • keep the increments as they are and remove the extra multiplication here.

Comment on lines +275 to +276
confidence = 0.0 if self._tol <= 0 else max(
0.0, min(1.0, 1.0 - self.last_residual / max(self._tol, 1e-12)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: The confidence computation includes dead branches and redundant safeguards.

Because the constructor already enforces tol > 0.0 (if tol <= 0.0: raise ValueError), both self._tol <= 0 and max(self._tol, 1e-12) are unreachable/unused cases. You can simplify to:

confidence = max(0.0, min(1.0, 1.0 - self.last_residual / self._tol))

This preserves behavior under the existing class invariant while making the confidence calculation more direct.

return agents, reports


class TestEigenTrustProperties:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider varying the number of pretrusted agents in the property-based tests

Right now the property-based tests always use pretrusted = [agents[0]], but the behavior of p and invariants like pretrusted mass lower bound and Sybil resistance depend on the structure of the pretrusted set. It would be useful to exercise several shapes of pretrusted.

You could, for example, extend _trust_graph (or add a small strategy) to draw pretrusted like:

pretrusted = draw(
    st.lists(
        st.sampled_from(agents),
        min_size=1,
        max_size=min(3, len(agents)),
        unique=True,
    )
)

and reuse this in the property tests that reason about p to broaden coverage without much added complexity.

Suggested implementation:

    for _ in range(edge_count):
        i = draw(st.integers(min_value=0, max_value=n - 1))
        j = draw(st.integers(min_value=0, max_value=n - 1))
        kind = draw(st.sampled_from(["positive", "negative"]))
        if i == j:
            continue
        reports.append((f"a{i}", f"a{j}", kind))

    pretrusted = draw(
        st.lists(
            st.sampled_from(agents),
            min_size=1,
            max_size=min(3, len(agents)),
            unique=True,
        )
    )

    return agents, reports, pretrusted
    @settings(
        max_examples=80,
        deadline=None,
        suppress_health_check=[HealthCheck.function_scoped_fixture],
    )
    @given(_trust_graph())
    @pytest.mark.asyncio
    async def test_simplex_and_row_stochastic(
        self,
        graph: tuple[list[AgentId], list[tuple[str, str, str]], list[AgentId]],
    ) -> None:
        agents, reports, pretrusted = graph
  1. Update all other usages of _trust_graph() in this file to unpack three values (agents, reports, pretrusted = graph) and fix any type hints accordingly.
  2. In property tests that currently construct pretrusted = [agents[0]] (or similar), remove that construction and instead use the pretrusted drawn from _trust_graph() so that the tests exercise varying pretrusted sets.
  3. If there are helper functions that accept (agents, reports) from _trust_graph(), update their signatures (and call sites) to also accept pretrusted or to accept a single graph tuple and unpack internally.

mariagorskikh pushed a commit that referenced this pull request May 26, 2026
…dian

`_build_consensus` was reporting `sum(per-dim medians)` as the score, while
`total_median` in the JSON used `median_low(per-judge totals)`. In any
non-degenerate case these diverge — e.g., PR #2 was showing
"scored 20.0/30" in prose while `median: 21.0` in JSON, confusing
downstream consumers.

- Plumb `total_median` (the value that actually gets written to JSON)
  through to `_build_consensus` so the prose uses the same aggregation.
- Add a regression test
  (`test_consensus_uses_total_median_not_sum_of_medians`) that constructs
  three judges with divergent score patterns where the old buggy
  computation (20) and the new correct computation (21) differ; it
  asserts the consensus string contains `f"{total_median:.1f}/30"`.
- Re-bootstrap `docs/hackathon/scores.json` with the fixed code; all 10
  entries now have the median field and consensus prose in agreement.
mariagorskikh pushed a commit that referenced this pull request May 26, 2026
…l-diff fixture

- judge_pr.py: skip temperature kwarg for gpt-5.x models (they only accept default=1)
- fixtures/hackathon-prs-2026-05-26.json: replace stub diffs with real git diffs
  (688-2002 lines per PR, fetched via git diff origin/main...origin/<head_ref>)
- docs/hackathon/scores.json: 10 PRs scored by 3 gpt-5.5 judges each via OpenAI.
mariagorskikh added a commit that referenced this pull request May 26, 2026
Integration of 5 platform tracks built in parallel by specialist agents:

- platform/ci-hygiene (PR #12): Makefile + pre-commit + idempotent CI feedback bot + CONTRIBUTING Definition of Done
- platform/open-problems (PR #13): 10 differentiated open problems across 10 layers, charter, judging doc
- platform/judge-panel (PR #14): rubric, anthropic + openai providers, run_all CLI, real-diff fixture, live gpt-5.5 scoreboard for PRs #2-#11
- platform/research-harness (PR #15): conditions matrix, claude-CLI live runner, collect + analyze, dry-run fixtures + tests
- platform/marketplace-ui (PR #16): /hackathon Next.js section with author tags, judge scores, layer browser; Python data adapter

Schema reconciled end-to-end (rubric -> scores.json -> adapter -> TS types -> UI) on the 6-dim 1-5 scale with totals in [6, 30].

Local CI: 341 passed, 1 skipped (matplotlib gated), 1 deselected (live marker).

Live judge scoreboard top:
  #2  harvard-phd     trust       26.0/30  (EigenTrust + checkable invariants)
  #7  coinbase-crypto payments    26.0/30  (HTLC escrow)
  #6  stanford-ml-phd trust       25.0/30
  #11 google-staff    transport   25.0/30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants