Repository navigation
feat(mentions): tag victim protocol's official X account in hack posts - #16
Merged
Merged
Conversation
Foundation for tagging the victim protocol's official X account in hack posts. Resolution + render will live in Backlight compose (chunks 2-3); the detector stays untouched, so the detection hot path is unaffected. - protocol_mention_store + mention_override tables (idempotent IF NOT EXISTS DDL -> auto-migrates existing deployments) - x_id (immutable numeric Twitter id) is the anchor; x_handle/website are mutable/optional - cmd/seed-mentions ingests the Surf export CSV: handles the "없음" missing-encoding and UTF-8 BOM, normalizes multi-handle cells - Surf groups a parent + its name variants/products under one project_id (Curve/Curve DAO, Binance/Binance Staked SOL); collapse to one canonical entity while preserving every name as an alias for the writer to match on - mistag guards: shared-handle collisions -> review; curated landmines (Libra/@jmilei personal, Binance/@bnbchain parent) -> suppress Phase 2 freshness/self-healing tracked in #13 #14 #15. Verified seed: 6245 entities, handle 99.12% / x_id 97.47%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add internal/mention: an in-memory index over the store that maps a raw
protocol name (engine output) to a tag/plain-text decision. Tags only on
a unique policy=allow match; ambiguous/miss/suppress/review -> plain text
("맞으면 태그, 애매하면 평문").
- name/alias/slug match + generic-suffix stripping ("IPOR Protocol" ->
"IPOR") + token fallback to widen coverage without fuzzy substring match
- store.AllMentionEntities loads the index once (no hot-path DB call)
Harden the seeder against a class of Surf data errors the real-data smoke
test surfaced: rows whose notes carry `defi_name_resolved_as:X` where X is
a DIFFERENT protocol (IPOR Protocol -> IQ Protocol's @QHUB_, Raft ->
Metakraft, Trevee -> Paladin) are flagged `review` so they never auto-tag.
Same-org variants (Curve -> Curve DAO) stay allowed via brand-prefix/token
overlap. Shared-handle + curated landmines unchanged.
Verified on full export: IPOR Protocol -> plain text; Curve/Lido/Sushi/
Aave still tag; 9 name-resolution mismatches flagged review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the resolver into both Backlight post renderers through a single shared mention.FormatTag so the two render paths cannot drift: - xfeed headline: "🚨 Curve (@curvefinance) exploit on Ethereum" - xpublish template: "Protocol: Curve (@curvefinance)" Not taggable (miss / suppress / review / ambiguous) -> plain protocol name, unchanged. The index is built once at service startup from store.AllMentionEntities (no hot-path DB call) and gated by HELIOS_VICTIM_MENTION (default on). A nil index = disabled = plain text, so every existing renderer test and the kill-switch-off path stay byte-identical. Until cmd/seed-mentions is run against the prod DB the store is empty, so every resolve misses -> plain text: this ships dormant and safe, and only activates once seeded + restarted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…S_DB_PATH
Commit the Surf export (seeds/surf-full-x-account-db.csv, 6,279 rows) and
default SURF_EXPORT_CSV to it, so no separate data hand-off is needed. An
operator with access to the service DB seeds the store with just:
HELIOS_DB_PATH=/path/var/backlight.db go run ./cmd/seed-mentions
SURF_EXPORT_CSV still overrides the path. Verified: default-path run seeds
6245 entities (handle 99.12% / x_id 97.47%).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
When Backlight posts a hack report to X, it now tags the victim protocol's official account so the protocol gets notified and our post reaches further:
When we can't confidently identify the account, the post stays plain text (
🚨 Curve exploit on Ethereum). Tagging the wrong account (a politician's personal account, an unrelated same-name project, a parent company) is worse than not tagging at all, so the rule is "tag only when sure, otherwise plain text."This ships dormant and safe:
HELIOS_VICTIM_MENTION(defaulttrue). Set it tofalseto force plain text everywhere.How it works (3 commits)
1.
333b6d5— store + seeder.A new SQLite store (
protocol_mention_store+mention_override) holds, per protocol: the official handle, the immutable numeric Twitter id, name/aliases/slug/token for matching, and amention_policy(allow/suppress/review).cmd/seed-mentionsloads it from the Surf export CSV. It also installs guards so dangerous rows never get tagged (details below).2.
7d939c1— resolver.internal/mentionbuilds an in-memory index from the store and maps a raw protocol name (whatever the engine output called it) to a decision: tag with@handle, or stay plain text. It tags only on a single confidentallowmatch; anything ambiguous, missing, suppressed, or under review → plain text.3.
2b14c07— render + kill-switch.Both post renderers (the live xfeed thread and the xpublish template) call one shared formatter,
mention.FormatTag, so they can't drift apart. The index is built once when the service starts and reused (no database call while composing a post).Tagging policy
@BNBCHAIN) are suppressed to avoid over-tagging.Name (@handle).Rollout
seeds/surf-full-x-account-db.csv, so no separate data hand-off is needed — from the repo root:SURF_EXPORT_CSVoverrides the path.HELIOS_VICTIM_MENTION=falsefirst, confirm posts are unchanged, then flip totrue+ restart.The seeder is idempotent (upsert), so re-running it on a fresh Surf export is safe.
Implementation notes & reviewer context
Resolution lives in Backlight (compose-time), not in the detector
The original design doc routed entity resolution to
hack-detector, arguing the detector "holdscontract_address + chainfirst." That premise is false in the code: the detector extracts onlyattacker_address+tx_hash+ aprotocol_namestring — it never extracts the victim contract. So resolution is done entirely in Backlight at compose time, which is already async/batched (runs minutes after detection, after the lumoskit RCA subprocess). Net effect: the detection hot path is untouched and gains zero latency. The detector still hands off just theprotocol_namestring it always did.Data foundation (Surf export)
Seeded from the Surf full export (6,279 entities). Verified coverage: handle 99.1%, numeric
x_id97.5%, website 2.4% (ignored — too sparse).x_id(immutable numeric Twitter user id) is the anchor, not the handle: handles get renamed/squatted, the numeric id doesn't. The seeder stores both; Phase 2 jobs usex_idto repair drifted handles.Mistag guards (this is the important correctness part)
"Tag only when sure" is enforced by three guards, not just a happy-path lookup:
suppress: e.g.Libra → @JMilei(a politician's personal account),Binance* → @BNBCHAIN(parent/chain account).review: when one handle maps to 2+ genuinely different protocols, none auto-tag (11 such groups in the real data).review: the export'snotescolumn carriesdefi_name_resolved_as:Xwhen Surf substituted a different protocol. The smoke test caught a real one:IPOR Protocolwas seeded with@QHUB_(which is actually IQ Protocol). The seeder flags these by comparing the two names — if they don't share a brand token / prefix, it's a cross-protocol substitution →review(9 such rows: IPOR/Raft/Pika/Trevee/…). Same-org variants (Curve→Curve DAO,TrueUSD→True USD) stayallow.The net guarantee: a unique
allowmatch is required to tag; everything else degrades to plain text.Canonical aggregation + aliases
Surf assigns one
project_idto a parent and its name variants/products (Curve/Curve DAO,Binance/Binance Staked SOL). The seeder collapses these to one canonical entity (keyed byproject_id) but keeps every name as an alias, so the resolver can still match on any of them. The displayed canonical name is the shortest variant (the base brand).Resolver matching
Exact normalized match on name / alias / slug, then a generic-suffix strip (
"IPOR Protocol"→"IPOR"; suffix set = protocol/finance/network, prefix-only so"raft"inside"Metakraft"doesn't false-match), then token-symbol as a last fallback. No fuzzy substring matching anywhere — that's what produced the originalIPOR → IQclass of bug. A key mapping to >1 canonical = ambiguous = plain text.Gotchas worth knowing
없음(not empty string) and the CSV has a UTF-8 BOM — both handled in the seeder.CREATE TABLE IF NOT EXISTSon everystore.Open, so the two new tables auto-migrate on the next service start. (This pattern creates new tables but cannotALTERexisting ones — fine here since both tables are new.)nilmention index = feature disabled = plain text. Both renderers default to this, so every pre-existing renderer test stays byte-identical and the kill-switch-off path is a true no-op.File map
internal/store/schema.go— two new tables + indexes.internal/store/mentions.go— store types + upsert/policy/stats +AllMentionEntities(index loader).cmd/seed-mentions/main.go— Surf CSV → store, with the three guards.internal/mention/index.go— in-memory index,Resolve, and the sharedFormatTagformatter.internal/xfeed/runner.go,internal/xpublish/publisher.go— injectFormatTag(one call each).internal/config/config.go,cmd/helios/main.go—HELIOS_VICTIM_MENTIONflag + build the index once at startup.Pre-existing test failures (NOT from this PR)
go test ./...shows two reds that also fail onorigin/main(verified by stashing this branch's changes):internal/lumoskit/TestRunnerUsesLumoskitRepoRootForBinaryUnderBin(fails on the committed base too) andinternal/e2e/TestBacklightToBridgeToMCPReadArtifact(passes in isolation; flakes only under full-parallel./...). Every package this PR touches is green.Phase 2 (deferred to issues, not in this PR)
The store schema already carries the seams (
source,last_verified_at,reverify_due_at,status). The actual automation is tracked in #13 (drift repair: refresh handles viax_id), #14 (optional per-post revalidation), #15 (backfill: resolve protocols missing from the store). No stub/dead code was added for these.