Skip to content

Repository files navigation

TruthBeacon v2 — Corroborated AI Fact-Checking on GenLayer

TruthBeacon is a GenLayer Intelligent Contract for decentralized fact-checking. Anyone submits a claim together with candidate source URLs; GenLayer's validators independently fetch and judge each source, and reach Optimistic Democracy consensus on one deterministic final verdict, stored permanently on-chain with a full, auditable evidence trail.

This is a from-scratch redesign of a previously rejected version of this contract (see Reviewer Feedback Addressed below), and has since been deployed and tested live on GenLayer Studio (see Live Deployment).

v2.8 update: this Accepted submission's steward left a specific improvement suggestion — a stronger source-authority and freshness policy (see Source-Authority & Freshness Policy (v2.8) below). That mechanism is implemented, 111/111 offline-tested, and was deployed and exercised live on GenLayer Studio — see CHANGELOG.md § v2.8 for both transactions.

v2.9 update: the v2.8 submission's steward left a further suggestion — "a governance-controlled authority or credibility registry so deployments can evolve source policy without relying on the small built-in list." That mechanism is implemented (see Governance-Controlled Credibility Registry (v2.9) below), 146/146 offline-tested, and deployed and exercised live on GenLayer Studio at a new address — see Live Deployment below. (The first deployment attempt hit a real bug, Address double-wrapping in _require_governor, caught by that live transaction rather than the offline suite; it's fixed, the offline stub hardened so this bug class is now catchable offline too, and the corrected redeployment below is what's actually live — see CHANGELOG.md § v2.9.1 for the full story.)

This contract does not determine absolute truth. It deterministically evaluates whether multiple independent sources corroborate or refute a claim under GenLayer consensus rules. A Verified verdict means "enough independent, credible, reachable sources agreed" — not an infallible statement of objective fact.


Documentation Map

Document What's in it
README.md (this file) Quick start, interface reference, aggregation rule, reviewer feedback mapping
ARCHITECTURE.md Component diagram, execution flow, storage/consensus model, why prompt_comparative
SECURITY.md Full threat model — prompt injection, fake news, Sybil domains, fetch failures, known limitations
DESIGN_DECISIONS.md Every design choice: problem → solution → alternative considered → trade-offs
TESTING.md Offline tests, the unexecuted integration example, and live deployment evidence tiers
CHANGELOG.md Full version history from the v1 rejection through every review round
ROADMAP.md What's intentionally out of scope, and why, for each future direction
CONTRIBUTING.md How to contribute, and what not to change casually
REVIEWER_GUIDE.md Where every claim in this repo can be independently verified
PROJECT_OVERVIEW.md 5-minute executive summary
SUBMISSION_CHECKLIST.md Pre-submission checklist
RELEASE_NOTES_v2.md What v2 achieved, summarized
tests/README.md Test-file-by-test-file coverage index

Live Deployment

Current live address (v2.9.1 — governance-controlled credibility registry, bug-fixed): 0x2Db6a24f519611155046A72355752cF869A067Bb Public explorer (all transactions): https://explorer-studio.genlayer.com/address/0x2Db6a24f519611155046A72355752cF869A067Bb

Deployed with a single governor (0xBD767E1958928ff1CAaE1218dbCb4b2f2ada35F6, quorum = 1) via deploy tx 0xe27e4fc363758b07e76862c3f97df9bd6e9c7765828d80e3d2a40e5e6b6e42ec (FINALIZED). This is a redeployment of the corrected source — an earlier deployment at 0x859dF1aEAC8C45F36B72C22535B9f83139bf49c8 hit a real bug (Address double-wrapping in _require_governor) on its first live governance call; that bug is fixed in this deployment's source, and the fix is itself verified live below, not just offline. See CHANGELOG.md § v2.9.1 for the full story.

Transaction A — propose_registry_change, the corrected mechanism working live (tx 0x6f20f47acfa5aa06b90a0f4c83a3b865bdad7debe0ea90f5c4b9a2aefa59ae01, FINALIZED, SUCCESS): propose_registry_change("scam-example-test.example", "add"), called from the governor address. Result: Output: "0" — a proposal_id, returned without error (unlike the pre-fix deployment, which raised TypeError: cannot convert 'Address' object to bytes at this exact call). Since quorum is 1, the proposal finalized atomically in this same transaction.

Confirmation — get_registry_status: a follow-up read call for "scam-example-test.example" returned "low_credibility", confirming the proposal genuinely mutated on-chain state, not just that the call didn't error.

Proves: the entire governance mechanism — authorization via gl.message.sender_address, proposal creation, quorum-based auto-finalization, and the resulting credibility_registry mutation — works correctly end-to-end on real GenVM infrastructure, and the v2.9.1 bug fix specifically resolved the exact failure mode observed on the prior deployment.


Prior live address (v2.8 — source-authority + freshness policy): 0x93F0F657a008FC99a41149E444AA37a604A14580 Public explorer (all transactions): https://explorer-studio.genlayer.com/address/0x93F0F657a008FC99a41149E444AA37a604A14580

This is the v2.8 redeployment (see CHANGELOG.md) — a new address, since GenLayer Intelligent Contract source changes require redeployment. Both v2.8 mechanisms have been exercised live, not just offline-tested:

Transaction 1 — expected_domains source-authority policy (tx 0x1891eb4645f426774c0301e3e9c7069d6fc253747381ed7672d7ef710afb5296, FINALIZED): submit_claim("The Eiffel Tower is located in Paris, France.", [wikipedia.org, britannica.com, history.com URLs], expected_domains=["wikipedia.org", "britannica.com"]). Result (claim_id "0"): history.com was fetched and judged Supported exactly like the other sources, but — because it was not in the declared expected_domains — it was correctly flagged is_authorized_domain: false and excluded from corroboration (unauthorized_domain_count: 1). britannica.com independently failed to fetch (fetch_status: inaccessible, a real network failure, correctly recorded rather than silently dropped). With only 1 authorized, reachable, credible source left (independent_domain_count: 1), the contract correctly returned final_verdict: InsufficientEvidence rather than letting the unauthorized source pad the count.

Transaction 2 — freshness / staleness signal (tx 0x760cdaa2fefec430d5b2896643b546255d500c9028c9bad01d759e4826e98a54, FINALIZED): submit_claim("The James Webb Space Telescope was launched in December 2021.", [a live Wikipedia article, a NASA page, and a 2018 Wayback Machine snapshot of the pre-launch Wikipedia article], expected_domains=[]). Result (claim_id "1"): the live Wikipedia source was judged Supported / freshness: "Current". The 2018 archived snapshot — describing JWST as a future, not-yet-launched project — was correctly judged freshness: "Stale", set is_stale: true, and excluded from corroboration (stale_source_count: 1), independently of the fact that its verdict also came back NotSupported. The NASA page failed to fetch (inaccessible, a real network failure). With only 1 eligible source, the result was again the conservative final_verdict: InsufficientEvidence.

Both transactions reached multi-validator consensus (ACCEPTEDFINALIZED) via gl.eq_principle.prompt_comparative, not exact string matching — transaction 2 in particular shows the comparator in action: several validators' raw outputs were marked Disagree against the leader's proposal before the round finalized, which is expected prompt_comparative behavior (validators judge equivalence under EQUIVALENCE_PRINCIPLE, not byte-identical output) and not an error.

The prior (pre-v2.8) deployment at 0xE30A0F67Da4a3F58F2E31C82dfbc50e8B8F588A5 remains as historical evidence for the base fetch/aggregate/consensus pipeline (clean Verified result, duplicate-domain detection, unanimous rejection, etc.) — full detail in REVIEWER_GUIDE.md and TESTING.md § Tier 3. Those transactions are not re-cited as v2.8 or v2.9 evidence; they remain valid only for the base pipeline they demonstrated.


Reviewer Feedback Addressed

"The contract checks whether one caller-selected page supports a claim, but it cannot establish that the source or claim is trustworthy. Add provenance checks or independent corroboration, retain auditable evidence, and test failure and adversarial-source cases."

Reviewer comment How this version addresses it
Checks only one caller-selected page submit_claim requires 3–6 candidate URLs (MIN_SOURCES_SUBMITTED/MAX_SOURCES_SUBMITTED). A single-URL submission is not a valid call.
Cannot establish source/claim trustworthiness Every source is scored independently by an LLM (Supported/NotSupported/Unclear); a fixed, on-chain denylist (LOW_CREDIBILITY_DOMAINS) flags known low-credibility domains, recorded but excluded from corroboration.
Add independent corroboration Verified/Refuted require at least 2 distinct-domain, successfully-fetched, non-denylisted sources to agree, with agreement strictly outnumbering disagreement. See Aggregation Logic below.
Add provenance checks _annotate_sources computes domain, validity, duplicate status, and denylist status for every URL before any network access, and this metadata is persisted on-chain in full.
Detect duplicate domains Domains are reduced to an approximate registrable form (_registrable_domain), so news.example.com, www.example.com, and mirror.example.com are recognized as the same source. Duplicates are recorded but excluded from corroboration. Verified live — see REVIEWER_GUIDE.md.
Retain auditable evidence get_claim(claim_id) returns the full record: claim text, final verdict, corroboration stats, and a per-source array with URL, domain, provenance flags, fetch status, and verdict for every submitted URL — nothing discarded.
Test failure cases Fetch failures are explicitly classified (timeout/inaccessible/empty/malformed), never silently treated as evidence. See TESTING.md.
Test adversarial-source cases Fake-news denylisting, duplicate/Sybil domains, conflicting sources, prompt injection (both claim text and source content), quoted/opinion/speculative content, and garbage pages are all covered. Full mapping in SECURITY.md.

For the complete history of how this was reached — including two critical self-review rounds and an SDK compatibility audit — see CHANGELOG.md.


Source-Authority & Freshness Policy (v2.8)

Steward's review note on the Accepted submission: "TruthBeacon is a substantive reusable fact-checking contract... A valuable next improvement would be a stronger source-authority and freshness policy so distinct domains provide more assurance of genuine independent corroboration."

Suggestion How this version addresses it Verified by
Source-authority policy locked at submission time, not resolution time New optional expected_domains: list[str] parameter on submit_claim. When provided, it's normalized and validated before any source is fetched, and only submitted sources whose domain is a member of that pre-declared, submission-time-locked set are eligible to count toward corroboration (is_authorized_domain in _aggregate). Every source is still fetched and recorded either way — nothing is silently dropped. Left empty (the default), behavior is identical to pre-v2.8: every domain is authorized. test_expected_domains_restricts_corroboration_to_declared_set, test_expected_domains_accepts_full_urls_as_entries, test_omitting_expected_domains_is_fully_backward_compatible, test_expected_domains_with_no_matching_sources_is_rejected_upfront (all in test_end_to_end.py); test_unauthorized_domain_not_counted_as_corroboration, test_missing_authorized_key_defaults_to_authorized (in test_aggregation.py)
Freshness signal, gating eligibility like is_duplicate_domain/is_low_credibility The per-source LLM prompt now asks for a second, independent line: a Current/Stale/Undated freshness judgment for the fetched content relative to the claim. _aggregate excludes any source flagged is_stale (i.e. not Current) from corroboration, exactly the same way it already excludes duplicates and denylisted domains. test_stale_source_excluded_from_corroboration_end_to_end, test_undated_source_excluded_from_corroboration_end_to_end, test_failed_fetch_freshness_is_not_applicable (in test_end_to_end.py); test_stale_source_not_counted_as_corroboration, test_undated_source_excluded_same_as_stale, test_two_fresh_supports_still_verify_with_a_stale_third, test_missing_freshness_key_defaults_to_not_stale (in test_aggregation.py); test_contains_freshness_guardrail (in test_prompt_and_consensus.py)

Backward compatibility: both new eligibility flags (is_stale, is_authorized_domain) are read in _aggregate with .get(key, safe_default) rather than direct indexing, so any pre-existing caller or hand-built record — including every test written before v2.8 — behaves exactly as it did before. submit_claim without expected_domains is unchanged in every other respect.

Verified live on GenLayer Studio (see Live Deployment above for both transactions, addresses, and full result breakdowns) — no longer offline-only.


Governance-Controlled Credibility Registry (v2.9)

Steward's review note on the Accepted v2.8 submission: "TruthBeacon provides a substantial reusable fact-checking primitive... A useful next refinement would be a governance-controlled authority or credibility registry so deployments can evolve source policy without relying on the small built-in list."

Suggestion How this version addresses it Verified by
Governance-controlled credibility registry, evolvable without redeployment New credibility_registry: TreeMap[str, str], an on-chain, mutable extension of the existing LOW_CREDIBILITY_DOMAINS baseline. A deploy-time-fixed committee of governors can propose (propose_registry_change) and vote (vote_on_proposal) to add or remove domains, with changes taking effect immediately once a strict-majority quorum is reached — no contract redeployment needed to evolve the denylist going forward. test_single_governor_deployment_auto_finalizes_on_propose, test_second_vote_reaches_quorum_and_finalizes, test_governance_added_domain_flagged_in_annotate_sources, test_governance_added_domain_excluded_from_corroboration_end_to_end (in test_governance.py and test_end_to_end.py)
The built-in list shouldn't itself become a single point of failure/control Governance can only ever extend the denylist, never weaken the permanent LOW_CREDIBILITY_DOMAINS baseline — propose_registry_change explicitly rejects any attempt to add or remove a baseline domain, so even a fully compromised governor committee can't un-flag theonion.com or similar. test_cannot_add_baseline_domain, test_cannot_remove_baseline_domain (in test_governance.py)
Not a single admin's unilateral decision Every registry change requires a strict-majority vote among the deploy-time-fixed governor set (quorum = floor(n/2) + 1), computed automatically and not caller-configurable. A single-governor deployment is still supported (quorum of 1) as a reasonable, simple starting point — but the same mechanism scales to a multi-governor committee without any code change. test_three_governors_has_quorum_two, test_two_governors_has_quorum_two, test_non_governor_cannot_propose, test_non_governor_cannot_vote (in test_governance.py)

Backward compatibility: _is_low_credibility(domain) — the single function both _annotate_sources and the persisted is_low_credibility flag now flow through — returns exactly what the old domain in self.LOW_CREDIBILITY_DOMAINS check did for any domain with no governance history. _aggregate itself required zero changes. Every pre-v2.9 offline test (all 111) passes completely unchanged.

Not backward compatible at the deploy layer, by necessity: the constructor now requires governors: list[Address] (previously took no arguments) — governance needs to know who's authorized from the moment the contract exists, so this genuinely requires a new deployment, exactly like every prior version bump. See Live Deployment above for the address and live transaction evidence.


Architecture (Summary)

Contract deployment: TruthBeacon(governors=[...])   # v2.9
        │      deploy-time-fixed governor committee, quorum = floor(n/2)+1
        │
submit_claim(claim_text, source_urls, expected_domains=[])
        │
        ├─ 1. Deterministic input validation (cheap, fails fast, no gl.* calls)
        │      claim_text ≤ MAX_CLAIM_TEXT_CHARS · 3 ≤ len(source_urls) ≤ 6
        │      ≥ 2 distinct, non-denylisted domains among submitted URLs
        │      expected_domains normalized + validated (v2.8, optional)
        │
        ├─ 2. Deterministic provenance annotation (_annotate_sources)
        │      registrable domain per URL · duplicate flag
        │      denylist flag, now via _is_low_credibility (v2.9: baseline
        │      OR governance-added) · is_authorized_domain (v2.8)
        │
        ├─ 3. ONE non-deterministic closure (gl.eq_principle.prompt_comparative)
        │      per source: fetch → classify → LLM judge → fixed-vocabulary
        │      verdict + freshness (v2.8) → deterministic aggregation
        │      (gated on staleness + authorization + credibility) → one
        │      final verdict + stats
        │
        └─ 4. Persist claim + verdict + full evidence trail to on-chain storage

separately: propose_registry_change / vote_on_proposal (v2.9)   # governor-only
        │      pure deterministic state logic - no gl.nondet.*/eq_principle.*
        │      needed, same as ordinary state-changing writes
        └─ mutates credibility_registry once quorum is reached

Full component diagrams, sequence diagrams, and the "why prompt_comparative not strict_eq" rationale: ARCHITECTURE.md.


Aggregation Logic

_aggregate only considers eligible records: fetch_status == "ok", not a duplicate domain, not low-credibility (baseline OR governance-flagged, v2.9), not stale/undated (v2.8), and authorized under the claim's declared source-authority policy if one was set (v2.8). Let support/oppose be the count of eligible Supported/NotSupported verdicts, and independent_total be the total eligible count.

Final verdict Exact condition
InsufficientEvidence independent_total < 2 — not enough independent, credible, fresh, authorized, reachable sources to say anything
Verified support >= 2 and support > oppose
Refuted oppose >= 2 and oppose > support
Disputed Neither above, but support > 0 and oppose > 0 (a tie or near-tie)
Unverified Everything else — enough sources exist but they're inconclusive

Tested nuance: Verified/Refuted require a strict majority, not unanimity — a 2-vs-1 split is still Verified, not Disputed (test_majority_with_dissent_still_verifies in tests/test_aggregation.py). Full rationale in DESIGN_DECISIONS.md § 6.


Public Interface

# Constructor (v2.9)
TruthBeacon(governors: list[Address])   # at least 1 required; duplicates deduped

# Fact-checking (unchanged public signature since v2.8)
submit_claim(claim_text: str, source_urls: list[str], expected_domains: list[str] = []) -> str   # returns claim_id
get_claim(claim_id: str) -> str      # full JSON evidence record
get_verdict(claim_id: str) -> str    # just the final verdict word
total_claims() -> int

# Governance (new in v2.9)
propose_registry_change(domain: str, action: str) -> str   # governor-only; action = "add" | "remove"; returns proposal_id
vote_on_proposal(proposal_id: str) -> str                  # governor-only; returns "pending" or "finalized"
get_proposal(proposal_id: str) -> str                       # full JSON proposal record
total_proposals() -> int
get_registry_status(domain: str) -> str                     # "low_credibility" | "not_flagged"
is_governor(address: str) -> bool
total_governors() -> int

expected_domains (v2.8, optional): a bare domain ("reuters.com") or full URL per entry; see Source-Authority & Freshness Policy (v2.8) above.

governors (v2.9, required at deploy time): a list of addresses forming the credibility-registry governance committee; see Governance-Controlled Credibility Registry (v2.9) above.

Example get_claim result:

{
  "claim_id": "0",
  "claim_text": "The Eiffel Tower is located in Paris, France.",
  "final_verdict": "Verified",
  "total_sources_submitted": 3,
  "independent_domain_count": 3,
  "duplicate_domain_count": 0,
  "failed_source_count": 0,
  "stale_source_count": 0,
  "unauthorized_domain_count": 0,
  "expected_domains": [],
  "sources": [
    {
      "url": "https://reuters.example/eiffel-tower",
      "domain": "reuters.example",
      "is_duplicate_domain": false,
      "is_low_credibility": false,
      "is_authorized_domain": true,
      "fetch_status": "ok",
      "verdict": "Supported",
      "freshness": "Current",
      "is_stale": false
    }
  ]
}

As of this revision, domain is the approximate registrable domain (see DESIGN_DECISIONS.md § 8), not necessarily the exact fetched hostname — the exact URL is always available in the url field. stale_source_count, unauthorized_domain_count, expected_domains, is_authorized_domain, freshness, and is_stale are from v2.8; is_low_credibility now reflects both the permanent baseline AND any governance-added registry entries (v2.9, see _is_low_credibility) but its field name and position in the schema are unchanged.


Security & Threat Model (Summary)

Prompt injection (both via fetched content and via the claim text itself), fake-news domains, Sybil-style duplicate-domain stuffing, fetch failures, weak/speculative evidence, and governance-mechanism abuse (v2.9) are all explicitly mitigated with tests. Full threat model, evidence, and residual risks: SECURITY.md.


Known Limitations (Summary)

No full Public Suffix List, no cross-domain content-similarity detection, no spam/staking defense, consensus reliability that inherently scales with source count, and a governance committee that is deploy-time-fixed (no on-chain mechanism yet to add/remove governors themselves without redeployment, v2.9). Every limitation is disclosed with its specific trade-off reasoning, not hidden: SECURITY.md § 8 and DESIGN_DECISIONS.md. What a future version could do about each: ROADMAP.md.


Testing (Summary)

146/146 offline tests passing, organized into 9 files by function under test:

python3 -m unittest discover -s tests -p "test_*.py" -v
File Tests Covers
test_domain_extraction.py 26 Registrable-domain extraction, subdomains, IPv6, trailing dots, expected_domains normalization (v2.8)
test_content_classification.py 11 Malformed/empty/ok content detection
test_aggregation.py 15 Final-verdict decision rule, incl. staleness + authorization gating (v2.8)
test_parser.py 8 Raw LLM response → fixed vocabulary
test_prompt_and_consensus.py 12 Prompt guardrails (incl. freshness, v2.8), equivalence principle
test_input_validation.py 9 Pre-fetch validation, gl.vm.UserError
test_end_to_end.py 23 Full pipeline, adversarial scenarios, expected_domains + freshness (v2.8), governance-added registry gating (v2.9)
test_governance.py 34 Constructor validation, proposal/voting lifecycle, baseline protection, _is_low_credibility integration (v2.9); incl. a regression test for a real live-caught Address-double-wrapping bug (v2.9.1, see CHANGELOG.md)
test_storage.py 8 On-chain persistence, multi-claim isolation

Plus an unexecuted gltest integration example (tests/gltest_integration_example.py — explicitly marked as not-yet-validated) and real live deployment evidence covering the base pipeline, the v2.8 source-authority/freshness policy, and (v2.9.1) the governance registry's core add/finalize path (see Live Deployment above). Full three-tier explanation: TESTING.md.


Deploying

Single-file deployment, same as any GenLayer Intelligent Contract — deploy contract.py via the GenLayer Studio "Create New Contract" UI. As of v2.9, the constructor requires one argument: governors, a JSON array of at least one address (e.g. ["0xYourAddressHere"]) — the initial credibility-registry governance committee. Pick this deliberately; while more governors can be represented by redeploying, there is currently no way to add/remove individual governors from an already-deployed contract without a new deployment (see SECURITY.md and ROADMAP.md).

Both the v2.9.1 and v2.8 addresses are deployed and live — see Live Deployment above for both, including live governance-registry transaction evidence for v2.9.1.


Repository Structure

truthbeacon/
├── contract.py                   # the Intelligent Contract (single deployable file)
├── README.md                     # this file
├── ARCHITECTURE.md
├── SECURITY.md
├── DESIGN_DECISIONS.md
├── TESTING.md
├── CONTRIBUTING.md
├── CHANGELOG.md
├── ROADMAP.md
├── REVIEWER_GUIDE.md
├── PROJECT_OVERVIEW.md
├── SUBMISSION_CHECKLIST.md
├── RELEASE_NOTES_v2.md
└── tests/
    ├── README.md                 # test coverage index
    ├── _bootstrap.py             # shared offline-stub wiring
    ├── genlayer_stub/            # minimal offline genlayer SDK stub
    ├── test_domain_extraction.py
    ├── test_content_classification.py
    ├── test_aggregation.py
    ├── test_parser.py
    ├── test_prompt_and_consensus.py
    ├── test_input_validation.py
    ├── test_end_to_end.py
    ├── test_governance.py
    ├── test_storage.py
    └── gltest_integration_example.py   # unexecuted, see TESTING.md

About

A GenLayer Intelligent Contract for corroborated AI fact-checking — requires multiple independent sources, detects duplicate/Sybil domains, enforces a source-authority policy, judges source freshness, and maintains a governance-controlled credibility registry, all under multi-validator consensus via gl.eq_principle.prompt_comparative.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages