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
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ name = "eigentrust"
version = "0.1.0"
edition = "2021"
authors = ["Jenya <hypnagonia@gmail.com>"]
description = "Rust and WASM Eigentrust implementation"
description = "Fast EigenTrust reputation algorithm: global trust scores for peer-to-peer networks and social graphs, native and WebAssembly"
homepage = "https://eigentrust.jenyadoesapps.com"
readme = "README.md"
keywords = ["eigentrust", "reputation", "trust", "sybil", "wasm"]
categories = ["algorithms", "wasm", "science"]
repository = "https://github.com/hypnagonia/eigentrust-rust"

[features]
Expand Down
47 changes: 27 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<h1 align="center">EigenTrust</h1>
<h1 align="center">EigenTrust in Rust and WebAssembly</h1>

<p align="center">
Global trust scores from local trust. Fast, in Rust and WebAssembly.
A fast implementation of the EigenTrust reputation algorithm.<br>
Global trust scores for peer-to-peer networks, social graphs and web-of-trust systems, with Sybil attack resistance built in.
</p>

<p align="center">
Expand All @@ -12,24 +13,31 @@
</p>

<p align="center">
<a href="https://eigentrust.jenyadoesapps.com"><img alt="EigenTrust playground" src="docs/playground.png" width="820"></a>
<a href="https://eigentrust.jenyadoesapps.com"><img alt="EigenTrust playground: an interactive trust graph ranked live in the browser with WebAssembly" src="docs/playground.png" width="820"></a>
</p>

## What it does
## What is EigenTrust?

- **Input:** who trusts whom (`alice,bob,3`), plus a few seed peers you already trust.
- **Output:** a score for every peer. The scores add up to 1.
- **Why:** trust spreads from the seeds, so fake accounts that only vouch for each other get almost nothing.
EigenTrust turns local trust ("alice trusts bob") into a global reputation score for every peer. Trust spreads from a few seed peers you already trust, so fake accounts that only vouch for each other get almost nothing.

Based on the [EigenTrust paper](https://nlp.stanford.edu/pubs/eigentrust.pdf) (Kamvar et al., 2003).
- **Input:** who trusts whom (`alice,bob,3`), plus seed peers.
- **Output:** a trust score for every peer. The scores add up to 1.
- **Algorithm:** power iteration on a sparse trust matrix, as in the [EigenTrust paper](https://nlp.stanford.edu/pubs/eigentrust.pdf) (Kamvar, Schlosser, Garcia-Molina, 2003). With no seeds it behaves like PageRank.

## Try it
## Use cases

**[eigentrust.jenyadoesapps.com](https://eigentrust.jenyadoesapps.com)**: edit a network, move the α slider, see the ranking update. You can also rank 250,000 peers in the browser.
- Reputation systems for peer-to-peer and decentralized networks
- Sybil and spam resistance in social graphs and communities
- Ranking accounts, contributors or nodes by who vouches for them
- Web of trust and endorsement graphs

Available in English, Español, 中文, हिन्दी, العربية, Português, Français, Deutsch, Русский and 日本語. The language follows your browser; you can change it in the header or with `?lang=`.
## Live demo

## Command line
**[eigentrust.jenyadoesapps.com](https://eigentrust.jenyadoesapps.com)**: build a trust network, move the α slider and watch the ranking update. You can also rank 250,000 peers in the browser.

Available in English, Español, 中文, हिन्दी, العربية, Português, Français, Deutsch, Русский and 日本語.

## Quick start (CLI)

```sh
cargo run --release -- ./example/localtrust.csv ./example/pretrust.csv [alpha]
Expand All @@ -42,7 +50,7 @@ bob,0.3333333134651184

α defaults to `0.5`. A higher α keeps trust closer to the seeds.

## Browser
## Use in the browser (WebAssembly)

```js
import init, { run } from './pkg/eigentrust.js'
Expand All @@ -60,29 +68,28 @@ Build with `./build.sh`. For a ready-made Web Worker, see [`demo/worker.js`](dem
| File | Line | Example |
| --- | --- | --- |
| Local trust | `from,to[,weight]` | `alice,bob,2` |
| Seeds | `peer[,weight]` | `alice,1` |
| Seeds (pre-trust) | `peer[,weight]` | `alice,1` |

- A header row is optional. Spaces, quoted fields, CRLF and a UTF-8 BOM are fine.
- Weights default to 1. Repeated `from,to` pairs: the last line wins.
- With no seeds, every peer starts equal (like PageRank).

## Performance

Random networks, 10 links per peer, CSV parsing included.
Random trust graphs, 10 links per peer, CSV parsing included.

| Peers | Links | CLI | Browser |
| ---: | ---: | ---: | ---: |
| 20,000 | 200,000 | 0.06 s | 58 ms |
| 100,000 | 1,000,000 | 0.4 s | 0.2 s |
| 250,000 | 2,500,000 | | 0.6 s |

The browser build uses all CPU cores when the page is cross-origin isolated (see [`demo/vercel.json`](demo/vercel.json)).
The browser build is multithreaded when the page is cross-origin isolated (see [`demo/vercel.json`](demo/vercel.json)).

## Development

```sh
cargo test --release # tests
./build.sh # WASM builds, copied into demo/
python3 -m http.server -d demo # run the playground locally
cargo test --release # tests
./build.sh # WASM builds, copied into demo/
python3 -m http.server -d demo # run the playground locally
git config core.hooksPath .githooks # once per clone
```
84 changes: 78 additions & 6 deletions demo/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ canvas.addEventListener('pointerup', (ev) => {
})

document.addEventListener('keydown', (ev) => {
if (ev.target.closest('input, textarea, select')) return
if (ev.target.closest('input, textarea, select, [role="listbox"]')) return
if (ev.key === 'Escape') select(null)
if ((ev.key === 'Delete' || ev.key === 'Backspace') && state.selected) removePeer(state.selected)
if (ev.key === 's' && state.selected) toggleSeed(state.selected)
Expand Down Expand Up @@ -907,17 +907,89 @@ function localizeNumbers() {

function applyLanguage(code) {
setLanguage(code)
$('lang').value = lang()
renderLangMenu()
localizeNumbers()
setAlpha(state.alpha)
renderPanel()
wake()
}

$('lang').replaceChildren(...Object.entries(LANGUAGES).map(([code, name]) => el('option', { value: code, lang: code }, name)))
$('lang').addEventListener('change', (ev) => {
saveLanguage(ev.target.value)
applyLanguage(ev.target.value)
// language menu: a button that opens a listbox, keyboard and pointer friendly
const langButton = $('langButton')
const langList = $('langList')
const langCodes = Object.keys(LANGUAGES)
let langActive = 0

function renderLangMenu() {
const cur = lang()
$('langCurrent').textContent = LANGUAGES[cur]
$('langCode').textContent = cur.toUpperCase()
langButton.setAttribute('aria-label', `${t('language')}: ${LANGUAGES[cur]}`)
langList.setAttribute('aria-label', t('language'))
langList.replaceChildren(...langCodes.map((code, i) => el('li', {
id: 'lang-opt-' + code,
role: 'option',
'aria-selected': String(code === cur),
onclick: () => chooseLang(code),
onpointermove: () => setLangActive(i),
}, el('span', { lang: code, dir: 'auto' }, LANGUAGES[code]), code === cur ? checkIcon() : null)))
}

function checkIcon() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('viewBox', '0 0 12 12')
svg.setAttribute('width', '12')
svg.setAttribute('height', '12')
svg.setAttribute('aria-hidden', 'true')
svg.classList.add('check')
svg.innerHTML = '<path d="M2 6.5 4.8 9 10 3"/>'
return svg
}

function setLangActive(i) {
langActive = (i + langCodes.length) % langCodes.length
langList.querySelectorAll('li').forEach((li, j) => li.classList.toggle('is-active', j === langActive))
langList.setAttribute('aria-activedescendant', 'lang-opt-' + langCodes[langActive])
}

function openLangMenu() {
langList.hidden = false
langButton.setAttribute('aria-expanded', 'true')
setLangActive(langCodes.indexOf(lang()))
langList.focus()
}

function closeLangMenu(focusButton = true) {
if (langList.hidden) return
langList.hidden = true
langButton.setAttribute('aria-expanded', 'false')
if (focusButton) langButton.focus()
}

function chooseLang(code) {
closeLangMenu()
if (code === lang()) return
saveLanguage(code)
applyLanguage(code)
}

langButton.addEventListener('click', () => (langList.hidden ? openLangMenu() : closeLangMenu()))
langButton.addEventListener('keydown', (ev) => {
if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') { ev.preventDefault(); openLangMenu() }
})
langList.addEventListener('keydown', (ev) => {
const keys = { ArrowDown: 1, ArrowUp: -1 }
if (ev.key in keys) setLangActive(langActive + keys[ev.key])
else if (ev.key === 'Home') setLangActive(0)
else if (ev.key === 'End') setLangActive(langCodes.length - 1)
else if (ev.key === 'Enter' || ev.key === ' ') chooseLang(langCodes[langActive])
else if (ev.key === 'Escape') closeLangMenu()
else if (ev.key === 'Tab') { closeLangMenu(false); return }
else return
ev.preventDefault()
})
document.addEventListener('pointerdown', (ev) => {
if (!langList.hidden && !$('langMenu').contains(ev.target)) closeLangMenu(false)
})

// ---------- start ----------
Expand Down
10 changes: 9 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,15 @@
EigenTrust
</a>
<nav class="links">
<select id="lang" class="lang" data-i18n-aria="language"></select>
<div class="langmenu" id="langMenu">
<button type="button" class="lang-button" id="langButton" aria-haspopup="listbox" aria-expanded="false" aria-controls="langList">
<svg class="globe" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9s1.3-6.4 3.8-9z"/></svg>
<span class="lang-name" id="langCurrent">English</span>
<span class="lang-code" id="langCode" aria-hidden="true">EN</span>
<svg class="chevron" viewBox="0 0 12 12" width="10" height="10" aria-hidden="true"><path d="M2.5 4.5 6 8l3.5-3.5"/></svg>
</button>
<ul class="lang-list" id="langList" role="listbox" tabindex="-1" hidden></ul>
</div>
<a href="https://github.com/hypnagonia/eigentrust-rust" data-i18n="source">Source</a>
<span class="profiles">
<a class="icon-link" href="https://www.linkedin.com/in/zunso/" target="_blank" rel="noopener" aria-label="LinkedIn" title="LinkedIn">
Expand Down
67 changes: 62 additions & 5 deletions demo/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
--seed: #c98a0e;
--error: #b8412f;
--focus: #3a3fd0;
--chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%235b6875' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
color-scheme: light;
}

Expand All @@ -26,6 +27,7 @@
--seed: #f0b43c;
--error: #ef7b66;
--focus: #8e93ff;
--chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%238d9aa6' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
color-scheme: dark;
}
}
Expand All @@ -42,6 +44,7 @@
--seed: #f0b43c;
--error: #ef7b66;
--focus: #8e93ff;
--chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' fill='none' stroke='%238d9aa6' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
color-scheme: dark;
}

Expand Down Expand Up @@ -85,16 +88,57 @@ button, select, input, textarea { font: inherit; color: inherit; }
.brand-edge { stroke: var(--trust); stroke-width: 2.5; }

.links { display: flex; align-items: center; gap: 20px; }
.lang {
width: auto;
padding: 3px 8px;
/* language menu */
.langmenu { position: relative; }
.lang-button {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 5px 11px;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--muted);
font-size: 14px;
line-height: 1.3;
}
.lang:hover { color: var(--ink); border-color: var(--muted); }
.lang-button:hover, .lang-button[aria-expanded="true"] { color: var(--ink); border-color: var(--muted); }
.lang-button svg { display: block; flex: none; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; }
.lang-button .globe { stroke-width: 1.6; }
.lang-button .chevron { stroke-width: 1.8; transition: transform .15s ease; }
.lang-button[aria-expanded="true"] .chevron { transform: rotate(180deg); }
.lang-code { display: none; font-weight: 600; font-size: 12.5px; letter-spacing: 0.02em; }

.lang-list {
position: absolute;
top: calc(100% + 6px);
inset-inline-end: 0;
z-index: 20;
min-width: 176px;
margin: 0;
padding: 5px;
list-style: none;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 12px;
box-shadow: 0 12px 32px -12px rgb(22 33 43 / 0.28);
}
.lang-list[hidden] { display: none; }
.lang-list:focus { outline: none; }
.lang-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 7px 10px;
border-radius: 8px;
cursor: pointer;
font-size: 14.5px;
color: var(--ink);
}
.lang-list li.is-active { background: var(--trust-soft); }
.lang-list li[aria-selected="true"] { color: var(--trust); font-weight: 600; }
.lang-list .check { flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.profiles { display: flex; align-items: center; gap: 14px; padding-inline-start: 20px; border-inline-start: 1px solid var(--line); }
.icon-link { display: inline-flex; padding: 2px; }
.icon-link svg { display: block; }
Expand Down Expand Up @@ -224,10 +268,20 @@ output { font-weight: 600; color: var(--trust); }
select {
width: 100%;
padding: 7px 10px;
padding-inline-end: 34px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--bg);
appearance: none;
-webkit-appearance: none;
background-color: var(--bg);
background-image: var(--chevron);
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 10px 10px;
cursor: pointer;
}
:root[dir="rtl"] select { background-position: left 12px center; }
select:hover { border-color: var(--muted); }

input[type="range"] { width: 100%; accent-color: var(--trust); }

Expand Down Expand Up @@ -319,6 +373,9 @@ textarea {
.stage-tools { inset-inline-start: 16px; bottom: 12px; }
.links { gap: 12px; }
.links > a[data-i18n="source"] { display: none; }
.lang-name { display: none; }
.lang-code { display: inline; }
.lang-button { padding: 5px 9px; gap: 6px; }
.profiles { padding-inline-start: 12px; gap: 12px; }
.legend { display: none; }
.topbar { padding: 12px 16px; }
Expand Down
Loading