Анализа на правичност
Како Pick a Winner избира случајни победници — и како секој може да провери било кое извлекување.
Последно ажурирано 2026-05-27 (v2 — supersedes v1 dated 2026-03-28)
За овој документ
Ова е нашата објавена методологија за избор на победници од коментари на објави на социјалните мрежи. Го документира методот на рандомизација, аудит-трагата, заштитите за интегритет на податоците и механизмите преку кои секој учесник или организатор на наградна игра може независно да провери извлекување. Тоа е и документот што го даваме за правен совет како одговор на обвинение за нефер игра.
1. Извршно резиме
Апликацијата Pick a Winner имплементира процес на избор на победници чии резултати се независно повторливи од која било трета страна и чија историја е криптографски усидрена во Биткоин блокчејнот. За секоја наградна игра извлечена на или по 2026-05-27, системот објавува:
- 256-битна криптографски-случајна семка (seed)
- SHA-256 хеш на сортираната листа на ID-а на квалификуваните коментатори
- Листа за преземање на тие ID-а
- Аудит-дневник поврзан со хеш-синџир за секоја административна акција врз наградната игра
- OpenTimestamps доказ за главата на аудит-синџирот кој, откако Биткоин ќе го потврди (~6 часа по настанот), не може да се измени без препишување на Биткоин блокчејнот
Секој може да ги вметне овие во Ruby код од пет линии и да ги пресмета истите победници; секој може да го пушти стандардниот ots verify CLI врз бајтите на доказот и да потврди дека аудит-историјата е усидрена во конкретен Биткоин блок. Овие две својства на независна репродукција заедно се цврста гаранција за правичност. Останатиот дел од документот ги опишува структурните заштити кои ги одржуваат.
Едноставно кажано: ова е гаранција дека „не можеме да предатираме или да препишеме резултати од наградна игра без вие да забележите“. Системот не може и не спречува доволно привилегиран администратор на базата на податоци да изврши деструктивен SQL. Она што го осигурува е дека секое такво препишување остава аудит-синџирот, објавените JSON снимки и Биткоин-усидрениот доказ во состојба која повеќе не се совпаѓа — несовпаѓање кое која било надворешна страна може да го забележи со ots verify и претходно зачувана снимка. Правичноста овде се спроведува преку детектабилност, а не преку доверба во операторот.
Што тврди овој документ:
- Репродуктивен случаен избор — секое извлекување ја запишува својата семка (еден цел број), канонската сортирана листа на ID-а и SHA-256 хеш на влезот; секој може да го повтори. Видете §3.
- Непроменливост на победниците на ниво на база — штом наградната игра ќе достигне статус
drawn(ова значи дека извлекувањето е извршено), колоните што ја содржат состојбата за победник / дисквалификација не можат да се изменат од апликациски код, сиров SQL или било што помалку од PostgreSQL суперкорисник кој го оневозможува тригерот. Видете §7. - Аудит-дневник само за додавање, поврзан со хеш-синџир — секоја административна акција е криптографски поврзана со својот претходник; секое бришење или препишување на аудит-ред го прекинува синџирот на откривлив начин. Видете §8.
- Дневна автоматска верификација на синџирот — позадинска работа го проверува синџирот за неодамна активни наградни игри и подига критичен
SystemAlertпри секое прекинување. Видете §8.4. - Биткоин-усидрени временски ознаки — главата на аудит-синџирот на секоја наградна игра се запишува на час во OpenTimestamps и се потврдува во Биткоин блок во рок од ~6 часа. Доказот може а се преземе. Откако ќе се усидри, главата на синџирот не може да се помести без препишување на Биткоин блокчејнот. Видете §9.7.
- Само-за-читање јавен API за аудит-дневник — секој може да испита, сними и спореди (diff) целата историја на настани за која било наградна игра. Крајната точка е поставена на две еквивалентни патеки (со клуч-токен и со slug) така што читател кој пристигнал преку која било од двете јавни URL-адреси за резултати може да стигне до аудит-дневникот со додавање на еден сегмент во патеката. Манипулацијата станува детектабилна за секој надворешен набљудувач во моментот кога ќе го испита. Видете §9.6.
- Чекор-по-чекор водич за верификација — посебна страница на /verify-fairness го води секој читател низ шест прогресивни нивоа на верификација, со експлицитно наведен праг на доверба за секое ниво.
- Помошник за верификација по наградна игра —
/results/<token>/verifyги собира сите URL-адреси потребни за проверка на една конкретна наградна игра на една страница. Видете §14. - Целосна транспарентност на учесниците — победниците, дисквалификуваните записи, целата датотека со квалификувани ID-а, семката и аудит-синџирот се јавно достапни од страницата за резултати на секоја наградна игра. Секоја наградна игра е достапна на две еквивалентни јавни URL-адреси:
/facebook_page_contests/<slug>(SEO-прилагодена URL-адреса што операторот обично ја споделува) и/results/<token>/<slug>(канонска URL-адреса со клуч-токен). Двете прикажуваат значка за верификација внатре во страницата; семката/хешот/симнувањата/фрагментот живеат на помошникот за верификација по наградна игра на/results/<token>/verify, на едно кликнување од било која влезна точка.
Што НЕ тврди овој документ:
- Наградните игри извлечени пред 2026-05-27 не носат запишана семка и не се ретроактивно проверливи. Јавната страница за резултати го прикажува ова чесно со известување „Извлекување пред зацврстувањето“. Видете §12.
- Аудит-настан понов од Биткоин-потврдата на својот усидрувач (~6 часа) е сè уште ранлив на решен PostgreSQL суперкорисник. Откако Биткоин ќе го потврди усидрувачот, таа ранливост се затвора. Видете §12.
- Интегритет на изворните податоци — Graph API одговорот што ја обезбедил листата на коментари не е криптографски потпишан од Facebook или Instagram. Системот може да докаже од што извлекувал, но не може да докаже дека тој влез се совпаѓал со она што било јавно објавено. Видете §12.
Каде всушност да се провери една наградна игра: видете §14 (Површини за јавна верификација) за целосниот каталог на URL-адреси и /verify-fairness за практичниот чекор-по-чекор. Помошникот по наградна игра на /results/<token>/verify е најлесната влезна точка ако имате конкретна наградна игра на ум.
2. Спроведувачот на изборот
Изборот на победници следи 8-чекорен секвенцијален конвејер. Секој чекор мора успешно да заврши пред да започне следниот. Кој било неуспех го запира целиот процес.
| Чекор | Име | Цел |
|---|---|---|
| 1 | ValidateToken | Потврди дека пристапот до Facebook/Instagram API е валиден |
| 2 | ResolvePlatform | Утврди дали е Facebook или Instagram; парсирај ја URL-адресата на објавата |
| 3 | FetchComments | Преземи ги сите коментари од објавата на социјалните мрежи преку официјалниот API |
| 4 | EnforceMinimumCommenters | Бара најмалку 100 уникатни коментатори за платени наградни игри |
| 5 | EnforceEntryLimit | Провери го бројот на коментари во однос на ограничувањето на нивото на корисникот |
| 6 | EnforceContestFee | Пресметај и побарај плаќање ако е применливо |
| 7 | FilterEntries | Примени ги правилата за квалификација врз сите записи |
| 8 | SelectWinners | Размешување со семка од квалификуваното мноштво |
Клучно својство: Спроведувачот („конвејор“) се активира или со кликнување на „Извлечи победници“ од организаторот на наградната игра или со закажаниот премин на AutoDrawContestsJob кога наградната игра ќе го достигне конфигурираниот ends_at или scheduled_draw_at. Од тој момент натаму процесот е целосно автоматизиран: организаторот не може да ги прескокне чекорите, да го измени квалификуваниот базен или да влијае на случајниот избор.
3. Алгоритам за случаен избор
3.1 Размешување со семка, репродуктивно
Секое извлекување е репродуктивно од запишана семка. Системот користи единствен детерминистички пат за размешување со семка (ова го заменува двојниот пат од v1 Array#sample / ORDER BY RANDOM(), кој беше статистички униформен но произведуваше нерепродуктивни резултати):
seed_hex = SecureRandom.hex(32) # 256 bits from the OS CSPRNG
rng = Random.new(seed_hex.to_i(16))
sorted_eligible_ids = eligible_entry_ids.map(&:to_i).sort
eligible_ids_hash = Digest::SHA256.hexdigest(sorted_eligible_ids.join("\n"))
total_to_select = number_of_winners + number_of_reserve_winners
selected = sorted_eligible_ids.shuffle(random: rng).first(total_to_select)
# First N items are winners 1..N in order; next M items are reserves 1..M.
Три својства:
Криптографски силна ентропија во семката. Семката доаѓа од SecureRandom.hex(32), кој чита од CSPRNG на оперативниот систем (/dev/urandom или getrandom(2) на Linux, BCryptGenRandom на Windows). 256 бита ентропија значи дека постојат 2^256 можни семки — практично непогодливо однапред, а самите битови на семката се статистички неразличливи од униформните. Униформноста на размешувањето е посебно својство (видете ја следната точка); двете тврдења не зависат едно од друго.
Униформен случаен избор. Array#shuffle е имплементација на Fisher-Yates подржана од доставениот Random примерок. Секоја пермутација на n елементи има веројатност 1/n!; секој запис има веројатност k/n да се појави на првите k позиции. Ruby-овиот Random е Mersenne Twister (MT19937), не CSPRNG — ова е намерно и ирелевантно за правичноста. CSPRNG статусот е важен само кога противникот може да набљудува делумен RNG излез и да го предвиди остатокот; во нашиот модел семката е непогодлив влез и целиот излез од размешувањето е објавен по извлекувањето, така што нема ништо за предвидување. Униформноста на Fisher-Yates е својство на алгоритамот при необјективен rand(n), не на криптографската сила на RNG-от.
Репродуктивно. Со дадената семка и канонската сортирана влезна листа, размешувањето е детерминистичко. Секој — ревизор, регулатор, учесник — може да го повтори фрагментот и да ги добие истите победници. Верзијата на Ruby е запишана на наградната игра (draw_ruby_version) така што идните инспектори знаат која имплементација на Array#shuffle го произвела резултатот. Меѓујазичната репродукција (Python, JavaScript, Go) е теоретски можна, но бара пре-имплементирање на Ruby-специфичното семирање на Mersenne Twister (init_by_array врз декомпозицијата на 64-битни парчиња на целобројната семка) и Ruby-специфичните правила за rejection-sampling на rand(n) — ниту едно од овие не доаѓа бесплатно од стандардната random библиотека на друг јазик. Затоа практичното упатство за верификаторите е „инсталирајте MRI Ruby ≥ 3.0“; страницата /verify-fairness тоа го објаснува. Со предна признаница за ова ограничување: бајт-компатибилната репродукција е Ruby-специфична, не јазично-агностичка.
3.2 Доделувањето на позиција Е размешувањето
Не постои втор чекор на рандомизација. Размешаниот редослед е редот на позициите: првите number_of_winners ID-а стануваат победници 1, 2, 3, … а следните 3 × number_of_winners ID-а стануваат резерви 1, 2, 3, …. Бидејќи Fisher-Yates произведува униформно случајна пермутација, секое доделување на позиција е и само униформно случајно; собирањето на двата чекори од v1 во едно размешување го поедноставува она што трет верификатор треба да го репродуцира.
3.3 Што се запишува при секое извлекување
При секое успешно извлекување, системот ги пишува следните колони во facebook_page_contests внатре во истата атомска трансакција која ја префрла наградната игра во статус drawn:
| Колона | Содржина | Цел |
|---|---|---|
draw_seed |
низа од 64 хекс-карактери | 256-битното RNG семка |
draw_eligible_ids |
JSONB низа од целобројни | Точната сортирана листа на ID-а користена како влез |
draw_eligible_ids_hash |
64-хекс-карактери SHA-256 | Дозволува на верификатор да провери дали листата на ID-а не била изменета |
draw_algorithm_version |
низа ("v1") |
За идна-доказна-проба: му кажува на верификаторот кој алгоритам да користи |
draw_ruby_version |
низа | Ја запишува Ruby верзијата под која првично било извршено размешувањето |
3.4 Атомичност
Самото размешување со семка е чиста пресметка во меморија (без запис во базата), па се извршува пред да се отвори трансакцијата; она што е атомично е зачувувањето на резултатот:
# Pre-transaction (deterministic given the seed; no DB state involved):
seed_hex = SecureRandom.hex(32)
sorted_eligible_ids = eligible_entry_ids.map(&:to_i).sort
eligible_ids_hash = Digest::SHA256.hexdigest(sorted_eligible_ids.join("\n"))
selected_ids = sorted_eligible_ids.shuffle(random: Random.new(seed_hex.to_i(16))).first(total)
ActiveRecord::Base.transaction do
# 1. Mark non-eligible entries as disqualified (with reason)
# 2. Reset winner-state columns on eligible entries (clears stale flags from any previous attempt)
# 3. Write is_winner / winner_position / reserve_winner_number on the selected IDs
# 4. Persist seed, eligible-ids list, hash, algorithm version, ruby version on the contest
# 5. Flip contest status to "drawn"
end
Ако кој било чекор внатре во трансакцијата не успее (грешка во базата, прекршување на ограничување, одбивање од тригер), целата трансакција се враќа назад. Нема делумна состојба — или секоја транскриптна колона и секој победник се запишани атомично, или ниту еден. Ако процесот падне меѓу размешувањето во меморија и BEGIN, ниедна состојба не е допрена и извлекувањето едноставно може повторно да се проба (новиот обид генерира свежа семка; стариот никогаш не бил набљудуван од никого).
4. Еден запис по лице
4.1 Ограничување на базата
Уникатен индекс осигурува дека секое лице може да има најмногу еден запис по наградна игра:
CREATE UNIQUE INDEX idx_contest_entries_unique_profile
ON contest_entries (facebook_page_contest_id, facebook_profile_id);
Ако истото лице коментира повеќе пати, системот користи PostgreSQL UPSERT за да ги задржи само најновите податоци за коментарот, истовремено одржувајќи единствен запис:
ContestEntry.upsert_all(records,
unique_by: [:facebook_page_contest_id, :facebook_profile_id])
4.2 Импликација за правичност
Лице кое коментира 100 пати има точно иста веројатност да победи како лице кое коментира еднаш. Ограничувањето на базата го прави структурно невозможно повеќе коментари да создадат повеќе шанси.
5. Филтрирање за квалификација
5.1 Типови правила
Организаторите на наградните игри ги дефинираат правилата за квалификација пред извлекувањето. Овие правила се применуваат униформно врз сите записи. Апликацијата прави разлика меѓу автоматски спроведени правила (извршени во кодот, со неуспеси кои произведуваат disqualification_reason) и рачно-проверливи правила (каде Facebook/Instagram Graph API не изложува сигурни податоци; овие се запишуваат како filter_warnings на наградната игра но не дисквалификуваат никого автоматски):
| Правило | Спроведување | Извор на податоци |
|---|---|---|
| Потребна е-пошта (само Facebook) | Авто — SQL: email IS NOT NULL AND email <> '' |
Текст на коментар од API |
| Опсег на датуми | Авто — SQL: (comment_time IS NULL OR comment_time >= starts_at) AND (comment_time IS NULL OR comment_time <= ends_at) — NULL временските ознаки се вклучуваат (видете 5.2) |
Временска ознака на коментар од API |
| Потребен клучен збор | Авто — SQL: LOWER(comment_text) LIKE '%keyword%' со sanitize_sql_like |
Текст на коментар од API |
| Минимум означувања | Авто — SQL: jsonb_array_length(COALESCE(message_tags, '[]'::jsonb)) >= N |
Message tags од API |
| Исклучи администратори на страницата (само Facebook) | Авто — API: листа на администратори од Facebook Graph API | Facebook API |
| Исклучи конкретни корисници | Авто — SQL: facebook_profile_id NOT IN (...) |
Листа доставена од корисникот |
| Исклучи претходни победници | Авто — SQL: меѓу-натпреварско прашање на записи за победници | База на податоци |
| Мора да се лајкне објавата | Само рачно — Facebook Graph API не изложува сигурно ID-а на оние што лајкале (видливи се само корисниците кои го авторизирале апликацијата); спроведувањето е означено како предупредување и оставено на организаторот да го провери | Не е применливо |
| Мора да се сподели објавата | Само рачно — Facebook sharedposts крајната точка повеќе не враќа податоци за објави на страници; спроведувањето е означено како предупредување |
Не е применливо |
| Мора да се следи страницата | Само рачно — листите на следачи не се сигурно изложени од Graph API; спроведувањето е означено како предупредување | Не е применливо |
| Исклучи деловни сметки | Моментално не се спроведува — колоната постои на наградната игра и може да се постави во формата, но ниту еден чекор од конвејерот не ја чита и не се емитува никаков filter_warnings запис. Третирајте го како no-op засега; организаторот мора да користи „исклучи конкретни корисници“ ако сака да отстрани одделни деловни страници. Видете ги follow-up за планираното решение. |
Не е применливо |
Трите рачно-проверливи правила се прикажуваат на страницата на наградната игра како filter_warnings известување до организаторот, така што организаторот знае дека системот не можел да ги верификува автоматски. Ова е материјално за секоја тврдина за правичност: ако суд праша дали „сите коментатори кои ја лајкале објавата“ биле вклучени во извлекувањето, искрениот одговор е дека системот не ги филтрирал не-лајкерите и, на наградни игри каде организаторот барал лајк, не-лајкерите остануваат во базенот освен ако организаторот рачно не ги исклучи преку правилото „исклучи конкретни корисници“.
5.2 Null и недостасувачки податоци
- NULL временски ознаки на коментари се вклучени намерно во филтерот за опсег на датуми. Некои одговори на Graph API (особено Instagram реплики и одредени стари записи) го изоставаат
created_time; исклучувањето на такви записи неправедно би дисквалификувало легитимни коментатори чија временска ознака не била вратена од API-то. - Совпаѓањето со клучни зборови е неосетливо на големина на букви и безбедно екранирано преку
ActiveRecord::Base.sanitize_sql_likeтака што специјалните LIKE метакарактери во клучни зборови доставени од корисникот се третираат литерално.
5.3 Примена на правилата
Сите правила се применуваат програмски. Организаторот на наградната игра не може рачно да вклучи или исклучи конкретни записи откако извлекувањето започнало. Правилата се дефинираат во моментот на креирање на наградната игра и се заклучуваат штом наградната игра ќе влезе во статус processing (уредувањето е блокирано од контролерот за секој статус различен од pending или failed). Ако извлекувањето не успее пред да се избере никаков победник, наградната игра се враќа во failed и организаторот може да ги уреди правилата пред повторен обид; бидејќи никогаш не биле запишани победници, ова не ги менува правилата на извлечена наградна игра.
5.4 Транспарентност на дисквалификацијата
Секој дисквалификуван запис носи disqualified: true ознака и disqualification_reason. Постојат точно два извори кои ја пополнуваат оваа колона и тие имаат различно потекло, различно авторство и различни импликации за одговорност:
Системски-генерирана причина за записи одбиени од филтер. Кога ќе се изврши размешувањето со семка, секој запис кој не ги поминал конфигурираните филтери за квалификација се ажурира со еден групиран запис со една генеричка причина — локализираниот еквивалент на „Не ги исполнил барањата на наградната игра“ (канонската англиска низа живее на services.winner_selector.did_not_meet во config/locales/en.yml). Истиот текст се пишува за секој запис одбиен од филтер во дадена наградна игра. Системот намерно не се обидува да припише конкретно правило кое не било исполнето по запис, бидејќи еден запис може истовремено да не исполни повеќе правила (пр. недостасува клучен збор и надвор од опсег на датуми) и издвојувањето на едно би било погрешно. Авторитативниот извор на вистина за зошто е одбиен даден запис е конфигурираниот сет на правила на самата наградна игра — секое активно правило и секој filter_warnings запис (§5.1) е јавно видлив на страницата за резултати, така што учесникот или ревизорот може да го спореди коментарот на записот со сетот на правила за да заклучи кое(и) правило(а) не го(и) исполнил.
Причина обезбедена од организаторот за повторно-извлечени победници. Кога организаторот на наградната игра ќе активира повторно извлекување според §7.3, причината во слободен текст што ја внесува преку формата за повторно извлекување се пишува во disqualification_reason на сменетиот победник. Тој текст е напишан целосно од организаторот, по сопствена дискреција на организаторот, во моментот на повторното извлекување. Pick a Winner го известува организаторот да запише причина и го складира тоа што ќе го напише; апликацијата не пишува нацрт, не предлага, не валидира, не уредува, не модерира и не контролира на друг начин содржината на причините обезбедени од организаторот. Секој спор, тврдина или жалба за формулацијата, точноста, правичноста или тонот на причината од организаторот — вклучително секоја тврдина дека погрешно претставува зошто е отстранет победник — е работа меѓу организаторот на наградната игра и засегнатиот учесник. Pick a Winner не одговара за содржината на дисквалификациониот текст напишан од организаторот и не прифаќа одговорност за решавање на такви спорови. EULA ја наведува соодветната договорна позиција во §12.5.1.
Овие податоци се видливи за сопственикот на наградната игра, на јавната страница за резултати и во CSV извозот. Откако наградната игра е drawn, и ознаката за дисквалификација и колоната за причина се DK-непроменливи (§7.2); единствениот аудитиран исклучок е патот redraw_winner!, кој е единствениот механизам преку кој ред на disqualification_reason може да се промени по извлекувањето — а тој се менува токму затоа што организаторот штотуку внел нова вредност.
6. Конкурентност и спречување на манипулација
6.1 Советодавни заклучувања
Секое извлекување добива PostgreSQL советодавно заклучување опсегнато на ID на наградната игра:
SELECT pg_try_advisory_lock(999001, contest_id);
Ова спречува две конкурентни извлекувања на истата наградна игра. Ако заклучувањето веќе е држено, вториот обид тивко се прескокнува. Заклучувањето се ослободува во ensure блок (гарантирано чистење). Посебно советодавно заклучување врзано за трансакција (AdvisoryLocks::AUDIT_EVENT_CHAIN, простор на име 999_005) ги серијализира вметнувањата во синџирот на аудит-настани по наградна игра, така што конкурентни пишувачи не можат да создадат два настани кои го делат истиот претходник (видете §8.2).
6.2 Машина на состојби
Наградната игра следи строга машина на состојби со валидирани премини (невалидни премини подигаат ContestStateMachine::InvalidTransitionError):
pending -> processing
processing -> drawn | pending (cancel) | payment_required | failed
payment_required -> processing
drawn -> completed | failed
failed -> processing
completed -> (terminal)
Преминот од processing во drawn може да настане само внатре во чекорот SelectWinners на конвејерот, во рамките на атомската трансакција која исто така ги запишува is_winner, winner_position, семката и транскриптот на квалификуваните ID-а. Не постои API крајна точка или корисничка акција која може директно да постави наградна игра во статус drawn.
6.3 Идемпотентност
Работата за извлекување проверува winners_drawn? пред да обработи. Ако победниците веќе биле избрани (од претходно извршување), работата излегува без акција. Ова спречува случајни повторни извлекувања.
6.4 Откажување
Организаторот на наградната игра може да откаже извлекување додека се обработува. Откажувањето поставува cancelled_at на записот на наградната игра, а конвејерот проверува за ова меѓу серии за време на преземањето на коментарите. Откажувањето ја враќа наградната игра во статус pending — не избира и не менува победници.
7. Непроменливост на победниците по извлекувањето
7.1 Моделот на закани
Откако ќе се запишат победниците, вредноста на која било тврдина за „правично извлекување“ зависи од запишаните победници да останат непроменети. Дизајнот од v1 (пред 2026-05-27) се потпираше на отсуството на крајна точка во апликацијата за менување на колоните на победниците; оператор со пристап до Rails конзолата сепак можеше да го преврти is_winner, winner_position или disqualified преку update!, update_columns или сиров SQL без да остави запис. Зацврстувањето кое слета на 2026-05-27 ја затвора оваа дупка на ниво на база.
7.2 PostgreSQL тригер
BEFORE UPDATE тригер на contest_entries се извршува на секоја UPDATE изјава. Ако поврзаната наградна игра е во статус drawn или completed, тригерот подига ако некоја од следните колони би се променила:
is_winnerwinner_positionreserve_winner_numberdisqualifieddisqualification_reason
Секоја колона штити различна нападна површина, и затоа сите пет се заклучени заедно, а не само водечкиот флаг is_winner:
is_winner— дали X воопшто бил избран? Превртувањето на ова е очигледниот напад за наместување.winner_position— кое ниво на награда го добил X? Заменувањето на позициите меѓу двајца постоечки победници (така што победникот на 1. награда станува победник на 2. награда и обратно) ги остава дватаis_winnerфлага наtrueи не би било фатено со чување само наis_winner.reserve_winner_number— која резерва е следниот повик ако примарен победник не одговори? Тивкото пренаредување на резервите по извлекувањето е свој клас напад.disqualifiedиdisqualification_reason— дали X бил исклучен и на која наведена основа? Ретроактивно дисквалификување на победник за да може повторно извлекување да унапреди избрана резерва, или препишување на наведена причина за да одговара на избран наратив, и двете би биле бескорисни наспроти чувари само на колоните на победниците.
Тригерот се активира пред редот да биде препишан, па PostgreSQL самиот ја одбива нарушувачката UPDATE. update!, update_columns, сиров SQL и update_all сите го удираат истиот тригер. Операторот може да го оневозможи тригерот само со извршување на DROP TRIGGER ... или SET session_replication_role = replica, и двете бараат PostgreSQL суперкорисник привилегии и се запишуваат од самиот PostgreSQL.
7.3 Единствениот легитимен исклучок: redraw_winner!
Повторно извлекување — замена на неодговорен примарен победник со следната резерва — е единствената легитимна модификација по извлекувањето. Методот redraw_winner! на моделот држи row lock на наградната игра, поставува override на ниво на трансакција (SET LOCAL pick_a_winner.allow_redraw = 'on') што тригерот го чита како дозвола, потоа ја пишува промената. Override-от:
- Е опсегнат на една трансакција со
SET LOCAL - Експлицитно се
RESETвоensureблок, така што не може да истече дури и когаredraw_winner!се повикува внатре во надворешна трансакција - Запишува
redraw_winnerаудит-настан (позиција, причина, стари/нови ID-а на профили на победници; §8) секогаш кога повикувачот доставуваperformed_by:. Единствениот продукциски повикувач — акцијатаredraw_winnerна контролерот — секогаш доставуваcurrent_user, па секое продукциско повторно извлекување е аудитирано. Повикувач што го изоставуваperformed_by:(пр. операција од Rails конзолата или иден автоматизиран пат) сепак го користи override-от и затоа сепак ја завршува замената на победник, но го прескокнува пишувањето на аудит-настан; видете ги follow-up за планираното затегнување кое го правиperformed_by:задолжителен и ја затвора оваа дупка.
Слободно-текстуалната reason запишана во метаподатоците на аудит-настанот — и копирана во disqualification_reason на сменетиот победник — е напишана од организаторот на наградната игра преку формата за повторно извлекување; Pick a Winner не генерира, не уредува или модерира овој текст. Видете §5.4 за соодветната рамка за откривање на правичност и EULA §12.5.1 / §12.7 за договорната позиција.
Не постои друг кодов пат кој го поставува override-от.
7.4 Што кого заменува
Изборот на замена е прво детерминистички, второ случаен, на начин кој ја зачувува правичноста:
1. Оригиналното извлекување (§3) случајно избира 3 × number_of_winners резервни записи во истата атомска трансакција како примарните победници. На секоја резерва се доделува reserve_winner_number во ред кој самиот бил униформно рандомизиран во моментот на извлекувањето. Резервите се зачувуваат пред да биде можно кое било повторно извлекување, па затоа секвенцата на замени е фиксирана во аудит-трагата од моментот кога ќе заврши извлекувањето.
2. При повторно извлекување, системот прво ја зема резервата со најнизок број што останува (ORDER BY reserve_winner_number ASC LIMIT 1 FOR UPDATE). Ова е детерминистичко за даден резултат од извлекувањето, но самиот редослед потекнува од униформен случаен процес запишан во објавениот транскрипт.
3. Само ако не остануваат резерви, системот се префрла на ORDER BY RANDOM() врз базенот на не-победници кои ги поминале оригиналните филтри. (Овој нерепродуктивен резервен пат се активира само откако сите 3N резерви ќе се исцрпат, што е ретко во практика.)
Во двата пата, организаторот не може да избере конкретна замена; може само да активира повторно извлекување за дадена позиција со задолжителна причина. Заклучувањето на ниво на ред FOR UPDATE спречува две конкурентни повторни извлекувања да изберат иста замена.
8. Аудит-дневник само за додавање, поврзан со хеш-синџир
8.1 Што се запишува
Секоја значајна акција создава ContestAuditEvent запис:
| Акција | Складирани метаподатоци |
|---|---|
draw_winners |
winner_count, total_entries, drawn_at, duration_ms, draw_seed, draw_algorithm_version, draw_eligible_ids_hash |
redraw_winner |
position, reason, old_winner_profile_id, old_winner_name, new_winner_profile_id, new_winner_name |
cancel |
cancelled_at |
winner_notified |
winner_id, winner_position, facebook_comment_id, facebook_reply_id, attempt_number, notified_at |
winner_notification_failed |
winner_id, winner_position, facebook_comment_id, attempt_number, error_class, error_message, attempted_at |
winner_marked_dmed |
winner_id, winner_position, marked_at |
winner_unmarked_dmed |
winner_id, winner_position, previous_notified_at, unmarked_at |
winner_email_delivered |
winner_id, winner_position, delivered_at |
Секој настан го складира корисникот кој дејствува (преку belongs_to :user), created_at временска ознака запишана од базата, и двете колони на хеш-синџирот воведени во §8.2. За извлекувања активирани од закажаниот AutoDrawContestsJob (наместо рачно кликнување на „Извлечи победници“), запишаниот корисник е сопственикот на наградната игра — системот нема одделен идентитет на актер. Читач кој ја испитува аудит-историјата треба затоа да го толкува полето user_id како идентификација на одговорната страна за наградната игра, не нужно човекот кој го иницирал овој конкретен настан.
8.2 Криптографско поврзување по настан
Секој ContestAuditEvent ред носи две колони додадени на 2026-05-27:
previous_hash—entry_hashна непосредно претходниот настан во синџирот на оваа наградна игра, или фиксен генезисен хеш ("0" * 64) за првиот настанentry_hash— SHA-256 врз канонска JSON серијализација на{previous_hash, action, facebook_page_contest_id, user_id, metadata, created_at}, со длабоко-сортирани JSON клучеви и временски ознаки во UTC со микросекундна прецизност за бајт-стабилен влез
before_create callback ги пресметува двете вредности внатре во pg_advisory_xact_lock(AUDIT_EVENT_CHAIN, contest_id) за да ги серијализира конкурентните вметнувања по наградна игра. Секоја наградна игра е независен дневник. Резултат: секоја тивка модификација или бришење на аудит-ред остава или сопствениот entry_hash на редот да не се совпаѓа со неговата пресметана вредност, или previous_hash на следниот ред да не се совпаѓа со entry_hash на изменетиот ред — и двата случаи се откриени од верификаторот кој шета по синџирот.
Глобален UNIQUE индекс на contest_audit_events.entry_hash обезбедува дополнителна бариера против судир/повторување низ сите наградни игри: дури и ако нападачот го оневозможи тригерот, не би можел да вметне ред кој повторно користи било која претходна вредност на entry_hash било каде во табелата. Комбинирано со микросекундна прецизност на created_at и советодавното заклучување по наградна игра, ова прави случајно или противничко повторно користење на хеш да биде прекршување на ограничување на ниво на база, наместо тивко прифатен запис.
8.3 PostgreSQL тригери
Два тригери спроведуваат однесување само-за-додавање на ниво на база:
BEFORE UPDATEнаcontest_audit_events— секогаш подига. Не постои GUC override; ажурирања никогаш не се дозволени.BEFORE DELETEнаcontest_audit_events— стандардно подига; го дозволува бришењето само подSET LOCAL pick_a_winner.allow_audit_purge = 'on', поставено одFacebookPageContest#before_destroy(prepend: true) иUser#before_destroy(prepend: true) така што легитимното каскадно уништување од родител може да заврши.
Намерниот исклучок за каскада на родител е документиран во lib/database_triggers.rb и е единственото исклучение од „аудит-настаните се засекогаш“.
Практичната импликација е дека аудит-синџирот на наградната игра не преживува бришење на самиот запис на наградната игра: каскада од User#destroy, админ чистење на напуштена сметка, повторно семирање на демо-корисник или експлицитно бришење на наградна игра ќе ги однесе и аудит-настаните на таа наградна игра (каскадата го превртува GUC override-от и тригерот тогаш го дозволува DELETE). Одбивањето на каскадата би оставило сирачиња аудит-настани кои покажуваат кон исчезнат facebook_page_contest_id, што е сопствен интегритетен проблем. Митигацијата против противничка употреба на овој исклучок е надворешна наместо внатрешна: секоја страна која повлекла снимка на audit-log.json пред уништувањето задржува независна копија на синџирот, а секој Биткоин-потврден .ots доказ симнат пред тогаш продолжува да се верификува наспроти Биткоин историјата без оглед на тоа што сега се наоѓа во базата на операторот. Видете §12.6 за ограничениот опис на заканата и планираниот follow-up за надворешно складиште на снимки кое би ја затворило преостанатата дупка.
8.4 Верификација
Две површини:
ContestAuditEvent.verify_chain!(contest)— шета по синџирот по редослед на id за една наградна игра, подигајќиContestAuditEvent::ChainBrokenпри првото несовпаѓањеrake audit:verify— шета по синџирот на секоја наградна игра и пријавува прекинувања; излегува со ненулта при неуспех
AdminDailyDigestJob извршува verify_chain! за секоја наградна игра активна во последните 7 дена. При било кое прекинување запишува критичен SystemAlert со 23-часовно деддуплирање, прикажувајќи го неуспехот пред администраторите.
8.5 Поединечни потврди за испорака до победниците
Додадено на 2026-05-28. Секое известување на победник остава поединечна потврда и соодветен аудит-настан, така што верификатор може да потврди дека операторот ги избрал и ги известил сите победници.
Три режима на известување, три јачини на доказ:
- Режим на одговор на коментар на Facebook (
no_email_facebook?) — најсилен. Системот објавува јавен одговор на оригиналниот коментар на секој победник преку Graphput_comment. Враќаниот ID на одговорот се сочувува во редот наcontest_entries(facebook_reply_id) иwinner_notifiedаудит-настан се запишува со ID-то на одговорот во неговите метаподатоци. Јавната страница со резултати прикажува линк „Верифицирано на Facebook“ за секој победник кој води директно до конкретниот одговор на објавата — секој може да кликне и да го види доказот во живо на самиот Facebook, надвор од базата на апликацијата. Ако поединечен повик не успее,winner_notification_failedнастан ја запишува класата и пораката на грешката; натпреварот останува во статусdrawnдодека секој победник нема успешна потврда. - Email режим за Facebook (
require_email?) —notified_atза секој победник се означува во моментот на ставање на email-от во редица. Набљудувач на ActionMailer (WinnerNotificationObserver) запишуваwinner_email_deliveredаудит-настан кога SMTP слојот навистина ја прифаќа пораката — тој настан е доказот за испораката и е поврзан со хеш-синџирот како секој друг настан. Ако SMTP не успее, набљудувачот не се активира; разликата помеѓу ставањето во редица (означено) и испораката (не означено) е видлива во аудит-дневникот. Адресата на примачот намерно не е вклучена во метаподатоците на настанот за да се избегне email PII во јавниот аудит-синџир. - Instagram режим (
instagram?) — потврда од сопственикот. Graph API на Facebook не изложува испраќање на Instagram DM. Сопственикот рачно праќа DM на секој победник надвор од апликацијата, потоа кликнува копче „Означи како DM-ирано“ за секој победник. Кликот запишуваwinner_marked_dmedаудит-настан кој ги идентификува победникот и времето на означување. Ова е сопствена потврда, а не надворешно проверлива признаница — но е поврзана со хеш-синџирот и временски означена, така што секое подоцнежно уредување од операторот би го прекинало синџирот на ист начин како и манипулиран настан за извлекување.
Идемпотентност. Одговарачот за коментари на Facebook е клучиран на facebook_reply_id IS NULL — повторното извршување на делумно неуспешна група само се обидува со победниците кои сè уште немаат потврда. Успешен одговор никогаш не се објавува двапати од страна на апликацијата. Тесниот преостанат прозорец за дупликат-одговор (Facebook прифатил, но одговорот се изгубил во транзит) е документиран во docstring-от на одговарачот и во tmp/followups.md како прифатено v1 ограничување.
Јавна површина за верификација. JSON крајната точка за аудит-дневник на results/:token/audit-log.json ги вклучува новите типови настани за известување (winner_notified, winner_notification_failed, winner_marked_dmed, winner_unmarked_dmed, winner_email_delivered) во нејзината events низа без филтрирање на ниво на апликација — секое известување се појавува заедно со draw_winners/redraw_winner/cancel и придонесува кон истиот хеш-синџир. Надворешен набљудувач кој ја испитува крајната точка ги гледа настаните за известување и неуспесите на известување во реално време.
8.6 Што ова фаќа, а што не
Фаќа:
- UPDATE на кој било аудит-ред преку апликацијата или преку сиров SQL (блокирано на тригерот)
- DELETE на кој било аудит-ред надвор од GUC-override-аниот пат на каскада од родител (блокирано на тригерот)
- Тивко инјектирање на фалсификуван аудит-ред (синџирот се прекинува при вметнување бидејќи верификаторот ги пресметува хешевите од канонски влезови)
- Пренаредување на настани за една наградна игра (покажувачот на претходник не се совпаѓа)
Не фаќа:
- PostgreSQL суперкорисник кој ги оневозможува тригерите, вметнува фалсификувани редови и ги пресметува сите низводни хешеви за да ја одржи конзистентноста на синџирот. Овој напад бара привилегии на ниво на база надвор од овластувањето на апликацијата и сепак би се појавил во сопствениот лог за прашања на PostgreSQL. Следниот слој на зацврстување — периодично усидрување на главите на синџирот во надворешно јавно складиште како git репозиторија или OpenTimestamps доказ — би спречил дури и овој напад правејќи го фалсификувањето детектабилно за секој надворешен набљудувач.
- Манипулација со изворните коментари преземени од Facebook/Instagram пред тие да влезат во базата. Graph API одговорот е изворот на вистина и не е криптографски потпишан од платформата.
9. Јавна проверливост
За читател-ориентиран преглед на секое ниво на верификација (од една-погледна значка до скриптирано следење на ретроактивна манипулација), видете го посветениот водич Проверете извлекување сами. Овој дел ги опишува површините на кои тој водич се однесува.
9.1 Двете јавни URL-адреси за резултати
Секоја извлечена наградна игра е јавно достапна на две еквивалентни URL-адреси:
- `/facebook_page_contests/<slug>` — сервирана од
FacebookPageContestsController#show. Ова е SEO-прилагодена URL-адреса што операторот обично ја објавува на социјалните мрежи; ова е она што се појавува во Facebook/Instagram прегледи на врски и резултати од пребарување. Нема токен во URL-адресата. - `/results/<token>/<slug>` — сервирана од
PublicResultsController#show. Канонската URL-адреса со клуч-токен. Идентична содржина; вклучена првенствено за директно поврзување од JSON на аудит-дневникот и за верификатори кои сакаат URL-адреса која е недвосмислена за тоа на која наградна игра се однесува токенот.
И двете страници ја прикажуваат истата компактна лента за верификација за наградни игри извлечени на или по 2026-05-27. Лентата прикажува:
- Статусна значка: Верифицирано, Повторно извлечено по извлекувањето, Откриена манипулација или Извлекување пред зацврстувањето
- Една-линиска тврдина („Независно проверливо од која било трета страна“)
- Врска „Како да проверите сами →“ која води до помошникот за верификација по наградна игра опишан во §9.2
Хекс транскрипти (семе, хеш, верзија на алгоритам, верзија на Ruby), врски за симнување и Ruby фрагментот не се прикажуваат внатре на било која од URL-адресите. Ова е намерна UX одлука: URL-адресата што операторот ја споделува останува чиста и читлива, додека едно кликнување го носи верификаторот до посветена страница каде се собрани сите технички артефакти. Самата значка се пресметува серверски од DrawVerifier при секое прикажување; не постои кеширана претстава која би можела да маскира манипулација.
За наградни игри извлечени пред 2026-05-27, лентата прикажува известување „Извлекување пред зацврстувањето — не е независно проверливо“ наместо значката Верифицирано. Ова е чесното откривање; видете §12.
9.2 Помошник за верификација по наградна игра
Трета јавна URL-адреса собира сè што е потребно на верификатор за една конкретна наградна игра:
/results/<token>/verify (сервирана од PublicResultsController#verify)
Страницата прикажува:
- Значката повторно, со експлицитна проза „Верифицирано со повторно извршување на размешувањето со семка“
- Верзија на алгоритам (
v1) и верзијата на Ruby под која се извршило извлекувањето - Целото 256-битно случајната семка (хекс)
- SHA-256 на сортираната листа на квалификувани ID-а
- Врска за симнување на
eligible-ids.txt - Врска до JSON на аудит-дневникот (§9.6)
- URL-адреса на последниот Биткоин-усидрувачки доказ (ако е усидрено) со висината на Биткоин блокот
- Меѓу-врски до чекор-по-чекор водичот (/verify-fairness) и овој документ
Врската „Како да проверите сами“ на компактната лента (на било која од двете јавни URL-адреси погоре) води овде. Времетраењето на кешот е 10 минути, така што преминот од на чекање → Биткоин-потврдено брзо станува видлив.
9.3 Како повторно да се изврши извлекување
Трета страна може да верификува секоја наградна игра по зацврстувањето во три чекори:
1. Посетете го /results/<token>/verify (или кликнете „Како да проверите сами →“ на која било страница за резултати).
2. Оттаму симнете го eligible-ids.txt и копирајте го објавената семка.
3. Извршете во IRB (или било кој Ruby >= 3.x):
require "digest"
seed = "PASTE_THE_PUBLISHED_SEED_HERE"
ids = File.read("eligible-ids.txt").split("\n").map(&:to_i).sort
# (1) Sanity check the input: must equal the published SHA-256.
Digest::SHA256.hexdigest(ids.join("\n"))
# (2) Re-run the shuffle. First N items are winners 1..N (in order);
# the next 3N items are reserves 1..3N.
ids.shuffle(random: Random.new(seed.to_i(16))).first(N_winners + N_reserves)
Овој фрагмент е знак-по-знак идентичен со оној на /verify-fairness Ниво 2 — копирањето од било кој од нив завршува на истиот код.
Ако хешот се совпаѓа И размешаните победници се совпаѓаат со она што страницата го прикажува, извлекувањето е верификувано. Ако едно од двете не успее, извлекувањето треба да се смета за манипулирано. Посветениот водич /verify-fairness ова го објаснува детално како Ниво 2.
9.4 Што уште е јавно
На страниците за резултати (§9.1) и на помошникот за верификација по наградна игра (§9.2), јавно прикажаната содржина вклучува:
- Наслов на наградната игра, опис, датуми, описи на наградите
- Победници, секој прикажан со позиција, име на профил, текст на коментар и обфусцирана е-пошта
- Дисквалификувани записи со нивната причина
- До 1.000 учесници по хронолошки редослед (со лекчо-пагинирање; целата нескратена листа е секогаш достапна за сопственикот на наградната игра преку CSV извозот опишан во §9.5)
Сите е-пошта адреси на јавната страница се обфусцирани; необфусцирани е-пошти се појавуваат само во CSV извозот достапен само за сопственикот.
9.5 CSV извоз
Сопственикот на наградната игра (и само сопственикот — не-сопствениците добиваат 404, а извозот е овозможен само откако наградната игра ќе достигне статус completed) може да ја извезе целата листа на записи како CSV датотека што содржи:
- Број на запис, име на учесник, ID на профил, е-пошта
- Текст на коментар
- Статус на победник (
Yes/No) иwinner_position - Статус на дисквалификација (
Yes/No) и причина за дисквалификација
CSV се стримува ред по ред од базата по хронолошки редослед на коментарите и содржи секој запис без капа на редови, обезбедувајќи целосен преносен запис на извлекувањето. 5-минутна пер-корисник пауза се применува за не-административни извози за ублажување на стругање.
Бидејќи CSV е само за сопственик, независен верификатор (секој освен сопственикот на наградната игра) кој сака целиот базен — над 1.000-записната јавна пагинација на страницата за резултати — го реконструира од јавниот eligible-ids.txt плус оригиналната објава на Facebook/Instagram. Затоа CSV е целосниот-записен артефакт на операторот; датотеката со квалификувани ID-а е на јавноста. Оваа поделба е намерна: CSV носи необфусцирани е-пошти кои операторот легитимно ги треба за достава на наградите; јавниот пат носи само информации потребни за репродукција и ревизија на извлекувањето.
9.6 API на синџирот за ревизија
Само-за-читање JSON крајна точка ја изложува целата хеш-поврзана аудит-историја на наградната игра (секој настан по синџирски редослед, со previous_hash, entry_hash, канонските метаподатоци и внатрешното ID на корисник на актерот). Крајната точка е поставена на две еквивалентни патеки така што читател кој стигнал на било која од двете јавни URL-адреси за резултати (§9.1) може да стигне до аудит-дневникот со додавање на еден сегмент во патеката на URL-адресата веќе во неговата адресна лента:
GET /results/:token/audit-log.json— спарена со канонската страница за резултати со клуч-токен.GET /facebook_page_contests/:slug/audit-log.json— спарена со SEO-прилагодената slug страница за резултати; ова е URL-адресата што операторите обично ја споделуваат.
И двете URL-адреси се сервираат од истиот ContestAuditChainSerializer и враќаат бајт-идентичен JSON (полињата contest_token, contest_slug, events, anchors и verification се пополнуваат на ист начин без оглед на патеката на барањето). Тест за контролерот ги фиксира оваа паритетност (test/controllers/facebook_page_contests_controller_test.rb — „slug audit-log.json payload matches the token audit-log.json payload“). Секое барање извршува ContestAuditEvent.verify_chain! серверски и враќа поле chain_status кое е или литералната низа "ok" или пораката на ChainBroken исклучок која го именува нарушувачкиот настан — пр. "previous_hash mismatch at event #5 (expected …, got …)" или "entry_hash mismatch at event #5". Одговорот се испраќа со Cache-Control: no-store така што тригер за манипулација е видлив во моментот кога ќе го извлече испитувачот.
Површината со две URL-адреси е чисто погодност за откривање. Податоците, проверката (winners_drawn? само), политиката за кеширање (no-store) и положбата на автентикација (никаква) се идентични на двете. proof_url полето на сидрото во JSON-от сè уште покажува кон крајната точка audit-anchors/:id.ots со клуч-токен без оглед на која патека го сервирала JSON-от, така што верификатор кој шета по врските од одговорот секогаш стигнува до истите бајти на доказ.
Што постигнува оваа крајна точка:
- Надворешно откривање на манипулација. Секој (учесници, новинари, регулатори, самиот оператор) може да ја испита крајната точка, да го сними вратениот синџир и да направи diff на последователни снимки. Секое бришење, вметнување или модификација на настан помеѓу снимки е видливо за држачот на претходната снимка — без оглед на тоа дали базата на дискот сè уште поминува сопствена внатрешна проверка на синџирот.
- Реал-ајм статус на синџирот. Бидејќи секое барање го пресметува повторно
chain_status, манипулацијата се открива од надворешен набљудувач во моментот на испитувањето, а не да чека до следното извршување наAdminDailyDigestJob. - Овозможено криптографско пресметување. Одговорот вклучува
verificationблок (алгоритам, редослед на полиња на payload, формат на временска ознака, генезисен хеш, правило за канонизација на JSON). Мотивиран верификатор може да го обнови канонскиот payload од изложените полиња и независно да го пресмета секојentry_hash. - Основа за надворешно усидрување. Надворешното усидрување на синџирот (следниот follow-up) запишува
entry_hashво надворешно јавно складиште. Крајната точка на аудит-дневникот ја обезбедува историјата на синџирот чија позиција овие усидрувачи ја докажуваат.
Што НЕ постигнува оваа крајна точка:
- Не спречува фалсификување. PostgreSQL суперкорисник кој ги оневозможува тригерите и ги препишува и
contest_entriesиcontest_audit_events(пресметувајќи го секој низводен хеш) ќе произведе чист синџир кој крајната точка го пријавува како"ok". Крајната точка само го прави фалсификувањето детектабилно — и само за страни што држат снимка од пред препишувањето. - PII површината е непроменета. Аудит-настаните складираат акција, позиција, ID-а на профили и имиња (веќе јавни на страницата за резултати) и внатрешен user_id на операторот (непрозирен цел број потребен за пресметување на хеш). Не се изложуваат нови лични податоци.
9.7 Биткоин-усидрени временски ознаки (OpenTimestamps)
Главата на аудит-синџирот на секоја наградна игра периодично се запишува во Биткоин блокчејнот преку OpenTimestamps. Часовна позадинска работа доставува дигест на (namespace || contest_id || latest_entry_hash) до неколку OpenTimestamps календарски сервери; шестчасовна работа го надградува секој доказ на чекање во полн Биткоин-усидрен доказ откако Биткоин блок ќе го потврди (обично во рок од ~6 часа од доставувањето).
Резултатот е изложен на две места:
audit-log.jsonодговорот (§9.6) добива низаanchors. Секој запис носи усидрен хеш на запис, временски ознаки на доставување и надградба, висина на Биткоин блок (откако ќе се надгради) и URL-адреса за симнување на сирови бајти на доказот.GET /results/:token/audit-anchors/:id.otsги сервира бајтите на доказот директно. Секој со стандардниот OpenTimestamps CLI може да извршиots verify -d <digest> <proof.ots>врз доказот за да потврди Биткоин усидрување без да верува на Pick a Winner, OpenTimestamps календарските сервери или на никого освен на самиот Биткоин блокчејн.
Зошто ова е важно: §12 го именува PostgreSQL-суперкорисник заканата како преостанато ограничување на зацврстувањето на ниво на апликација. Со Биткоин усидрувањето на место, кој било настан во синџирот чие усидрување е надградено (~6 часа по настанот) е структурно заштитен од препишување — операторот може да ја препише својата сопствена база, но не може да ја препише Биткоин историјата. Деталниот процес на верификација е на водичот Проверете извлекување сами како Ниво 5.
9.8 Задржување на податоци
- Податоците за наградната игра се задржуваат за животот на сметката
- Аудит-настаните се задржуваат за животот на наградната игра (со исклучок на каскадата од родителот во §8.3)
- Потврдените системски предупредувања се задржуваат 90 дена
- Бришењето на наградна игра трајно ги отстранува нејзините поврзани редови (каскада)
10. Што организаторот на наградната игра може и не може
МОЖЕ (пред извлекувањето):
- Конфигурира правила за квалификација: автоматски-спроведени правила (означувања, клучни зборови, опсег на датуми, потребна е-пошта, исклучи-конкретни-корисници, исклучи-претходни-победници, исклучи-страница-администратори) и рачно-проверливи правила (мора-да-лајкне / мора-да-сподели / мора-да-следи — информиран дека овие не можат автоматски да се спроведат; видете §5.1)
- Поставува број на победници (1–12); системот автоматски извлекува
3 × number_of_winnersрезерви заедно со примарните победници - Исклучува конкретни Facebook/Instagram ID-а на профили
- Исклучува претходни победници од други наградни игри на истата страница
- Закажува автоматско извлекување во одредено време (
scheduled_draw_at) или се потпира на крај-на-наградна-игра автоматско извлекување
НЕ МОЖЕ (во кој било момент):
- Избира или влијае на тоа кое конкретно лице ќе победи
- Менува квалификуван базен по започнување на извлекувањето (уредувањето е блокирано откако статусот ќе ги напушти
pending/failed) - Прескокнува чекор на рандомизација
- Повторно користи семка низ наградни игри (секое извлекување генерира свое од OS CSPRNG)
- Извршува извлекување додека е во тек друго извлекување (советодавно заклучување)
- Избира конкретна замена при повторно извлекување (редот на резервите е фиксиран при извлекување, резервниот пат е случаен)
- Менува или брише записи на аудит-трага преку апликацијата (DB тригери; §8.3)
- Менува колони на победник/дисквалификација на извлечена наградна игра (DB тригер; §7.2)
- Создава дупликат записи за фаворизиран учесник (уникатно ограничување во базата)
МОЖЕ (по извлекувањето):
- Повторно извлекува конкретна позиција со задолжителна причина (целосно аудитирано; замената се извлекува од претходно ангажираните резерви; §7.3)
- Извезува цела листа на записи како CSV (откако наградната игра е
completed; само за сопственик; 5-минутна пауза) - Споделува јавна страница за резултати (која сега ја вклучува површината за верификација; §9.1)
11. Преглед на технички гаранции
| Својство | Механизам | Може да се заобиколи? |
|---|---|---|
| Еднаква веројатност по запис | Array#shuffle (Fisher-Yates) врз засејан Random |
Не — алгоритамски, без кориснички влез |
| Репродуктивно извлекување | Семка + сортирана листа на квалификувани ID-а + нивниот SHA-256 складирани на наградната игра; објавени на јавната страница | Не — секој може да го повтори размешувањето и да потврди |
| Еден запис по лице | Уникатен индекс во базата на (facebook_page_contest_id, facebook_profile_id) |
Не — спроведено на DB ниво преку upsert_all |
| Без конкурентни извлекувања | PostgreSQL советодавно заклучување (простор на име 999_001, contest_id) | Не — кооперативно заклучување добиено пред да се изврши конвејерот |
| Атомичен избор на победник + резерва + транскрипт | Единствена ActiveRecord::Base.transaction |
Не — сè или ништо |
| Состојбата на победникот непроменлива по извлекувањето | BEFORE UPDATE PostgreSQL тригер на contest_entries, условен на статусот на наградната игра |
Само преку DB суперкорисник кој го оневозможува тригерот (запишано од PostgreSQL); GUC override-от redraw_winner! на апликацијата е единствениот аудитиран исклучок |
| Аудит-трага само за додавање | BEFORE UPDATE / DELETE PostgreSQL тригер на contest_audit_events |
Ажурирања: никогаш. Бришења: само преку документираниот GUC override на каскада од родителот |
| Аудит-трага со доказ за манипулација | SHA-256 хеш-синџир по наградна игра; верификуван дневно од AdminDailyDigestJob |
DB суперкорисник кој ги препишува сите низводни хешеви може да фалсификува — но за секој настан на синџирот покрај Биткоин усидрувачот (~6 часа прозорец за потврда), фалсификувањето би барало и препишување на Биткоин |
| Аудит-синџир усидрен во Биткоин | Часовно OpenTimestamps доставување + шестчасовно ажурирање на доказот (Биткоин самиот ги потврдува календарските партии секои ~10 мин, независно од нашата каденција); .ots доказот може да се презееме по сидро |
Не може секој со доказот може да изврши ots verify врз самиот Биткоин блокчејн |
| Јавна проверливост | Две еквивалентни јавни URL-адреси (/facebook_page_contests/<slug> и /results/<token>/<slug>) ја прикажуваат значката за верификација; помошникот за верификација по наградна игра на /results/<token>/verify изложува семка, хеш, преземачки ID-а, врска до аудит-дневник и последен сидрен доказ. JSON-от на аудит-дневникот е достапен на две еквивалентни патеки (со клуч-токен и со slug, §9.6) |
Секој со врската може независно да ги потврди победниците; не е потребна автентикација |
| Без рачен избор на победник | Автоматизација на конвејер без UI affordance | Нема UI, нема крајна точка на контролер и нема DB пат за пишување кој не поминува низ размешувањето со семка |
| Транспарентност на дисквалификацијата | Причина складирана по запис, прикажана јавно, во CSV и непроменлива по извлекувањето | Видливо на страницата за резултати; не може да се менува по drawn |
| Правичност на повторното извлекување | Резервите однапред извлечени во оригиналното време на извлекувањето (униформно рандомизирани преку истата семка); резервен пат на случајни не-победници само кога ќе се исцрпат резервите; секое повторно извлекување целосно аудитирано | Организаторот не може да избере замена; FOR UPDATE на ниво на ред спречува судир при конкурентни повторни извлекувања |
| Непроменливост на правилата по почетокот на извлекувањето | Контролерот блокира уредување/ажурирање за секој статус надвор од pending / failed |
Не; неуспешно извлекување може да се уреди пред повторен обид, но никогаш не се запишуваат победници на неуспешно извлекување |
12. Чесни ограничувања
Документот ја губи кредибилност ако тврди повеќе отколку што може да докаже. Секое ограничување подолу е именувано експлицитно, така што регулатор, суд или скептичен учесник може да одлучат дали преостанатиот ризик е прифатлив за нивната цел.
12.1 Наградни игри пред зацврстувањето (реално, ограничено по датум)
Наградните игри извлечени пред 2026-05-27 немаат запишана семка и не можат ретроактивно да се повторат. Јавната страница за резултати го прикажува ова со известување „Извлекување пред зацврстувањето — не е независно проверливо“. Нивните победници се реални — биле извлечени од истиот униформно-случаен конвејер — но трета страна не може да ги пресмета. Граница: строг датум на пресек; влијае само на стари наградни игри; секоја наградна игра извлечена по 2026-05-27 е проверлива.
12.2 Прозорец за сидро на чекање (реално, ~6 часа по настан)
Аудит-настан запишан помалку од ~6 часа порано е усидрен на OpenTimestamps календарските сервери но сè уште не е потврден во Биткоин блок. Во овој прозорец, решен PostgreSQL суперкорисник кој ги оневозможил тригерите би можел во принцип да го препише настанот и би требало само да ги залаже OTS календарските сервери (не Биткоин) за да фалсификува чист дневник. Откако Биткоин ќе го потврди сидрото (типично 1–6 часа по доставувањето), настанот е структурно заштитен — препишувањето би барало препишување на Биткоин блокчејнот, што не е под контрола на ниту еден оператор. Граница: влијае само на настани создадени во последните неколку часа; се чисти автоматски како што Биткоин потврдува. Значката на страницата по наградна игра ги разликува Биткоин-потврдените сидра (прикажана висина на блок) од оние на чекање.
12.3 Интегритет на изворните податоци (реално, структурно неизбежно)
Базенот на квалификувани коментатори се преземе од Facebook/Instagram Graph API, и одговорот на API-то не е криптографски потпишан од платформата. Системот може да докаже од што извлекувал — секој бајт што влегол во размешувањето со семка е запишан во draw_eligible_ids — но не може независно да докаже дека тој влез се совпаѓал со она што било јавно објавено. Самата објава на Facebook/Instagram останува изворот на вистина за листата на коментари. Граница: ова е својство на горните платформи, не на Pick a Winner; единственото поправање би барало Meta да почне да ги потпишува API одговорите.
12.4 Повторни извлекувања (реални, целосно аудитирани)
Повикот redraw_winner! го заменува примарниот победник со резерва. Оригиналниот транскрипт на извлекувањето сè уште се верификува како точен (запишаната семка + квалификувани ID-а ги репродуцираат оригиналните победници); јавната страница прикажува значка „Повторно извлечено по извлекувањето“ и повторното извлекување е целосно аудитирано со позиција, причина и стари/нови ID-а на профили на победници. Верификатор мора да го консултира аудит-дневникот за да го разбере сегашниот сет на победници по повторно извлекување. Граница: секое повторно извлекување е видливо, аудитирано и поврзано во истиот хеш-синџир како оригиналното извлекување.
12.5 Резервен пат при исцрпени резерви (реално, ретко во практика)
Резервниот пат од §7.4 ORDER BY RANDOM() за повторни извлекувања (активиран само откако сите 3N резерви ќе се исцрпат) не е репродуктивен од запишаниот транскрипт. Овој резервен пат е редок во практика — бара операторот да повторно извлече покрај секоја однапред-ангажирана резерва — и е целосно аудитиран. Граница: означено како посебно поле во метаподатоци на аудит-настан така што верификатор може да идентификува кои победници (ако ги има) дошле од резервниот пат.
12.6 Уништувањето на наградната игра го носи и аудит-синџирот (реално, ограничено со надворешни снимки)
Бришењето на FacebookPageContest каскадно ги отстранува редовите на contest_audit_events преку GUC override-от pick_a_winner.allow_audit_purge (видете §8.3); истата каскада исто така ги отстранува редовите на chain_anchors кои ги складираат локалните копии на OpenTimestamps доказите. По уништувањето, не останува внатрешен запис на аудит-синџирот или неговите сидра. Што преживува е она што трети страни го симнале порано: снимки на audit-log.json (целата историја на синџирот) и .ots бајти на доказ (кои продолжуваат да се верификуваат наспроти Биткоин историјата без оглед на тоа што сега се наоѓа во нашата база). Надворешен набљудувач кој испитува audit-log.json по било кој распоред задржува комплетна, структурно-неманипулативна копија; Биткоин-потврден доказ зачуван од таков набљудувач продолжува да го атестира постоењето на конкретни глави на синџирот на конкретни висини на блокови дури и по уништувањето на основниот запис на наградната игра. Граница: преостанатите докази живеат надвор од дофатот на операторот по дизајн. Митигациона патека: follow-up проектот „Надворешно складиште за снимки на транскрипт“ запишува write-only копија на секој транскрипт во надворешно складиште пред уништувањето да биде можно; неговото слетување ја затвора внатрешната дупка така што надворешните снимки стануваат редундантност наместо единствена линија на одбрана.
12.7 Што повеќе не е ограничување
Ставките претходно именувани во овој дел, кои сега се ублажени:
- „PostgreSQL суперкорисник може да произведе внатрешно конзистентен фалсификуван дневник“ — затворено со Биткоин усидрување за настани постари од ~6 часа (§9.7). Зацврстувањето на ниво на апликација само никогаш не ја победило оваа закана; Биткоин обврзувачот ја победува.
- „Надворешно усидрување на главата на синџирот е на патот“ — слета на 2026-05-27. Видете §9.7 и §15 (модел на закани).
13. Референци на изворен код
Целиот код е достапен за независна ревизија:
| Компонента | Патека на датотека |
|---|---|
| Избор на победник + резерва (размешување со семка, зачувување на транскрипт) | app/services/winner_selector.rb |
| Верификатор на извлекувањето (повторно го извршува размешувањето и споредува) | app/services/draw_verifier.rb |
| Филтрирање на записи (авто + предупредувања за рачна верификација) | app/services/contest_entry_filter.rb |
| Преземање на коментари (upsert уникатно по профил) | app/services/contest_pipeline/fetch_comments.rb |
| Оркестрација на конвејерот | app/services/facebook_contest_service.rb |
| Чекор на конвејерот — избор на победници | app/services/contest_pipeline/select_winners.rb |
| Работа за извлекување (советодавни заклучувања, осигурувач, аудит-настан) | app/jobs/draw_winners_job.rb |
| Дневна верификација на синџирот | app/jobs/admin_daily_digest_job.rb |
| Простори на имиња на советодавни заклучувања | config/initializers/advisory_locks.rb |
| Тригери на базата (непроменливост + само-за-додавање) | lib/database_triggers.rb |
| Машина на состојби | app/models/concerns/contest_state_machine.rb |
| Модел на записи | app/models/contest_entry.rb |
| Модел на аудит-настан (хеш-синџир, verify_chain!) | app/models/contest_audit_event.rb |
| Логика за повторно извлекување (прво-резерва, GUC override, аудит) | app/models/facebook_page_contest.rb (redraw_winner!) |
| Откажување (аудит-настан + премин кон pending) | app/services/contests/cancel_draw.rb |
| Јавни резултати + површина за верификација + API на аудит-синџирот (патека со токен) + крајна точка на доказ за сидро | app/controllers/public_results_controller.rb |
| API на аудит-синџирот (патека со slug; идентичен payload) | app/controllers/facebook_page_contests_controller.rb (audit_log акција) |
| Споделен JSON сериализатор на аудит-синџирот (единствен извор на вистина за двете API патеки) | app/services/contest_audit_chain_serializer.rb |
| Partial за UI верификација (компактна лента) | app/views/public_results/show/_verification.html.erb |
| Чекор-по-чекор страница на водич за верификација | app/views/pages/verify_fairness.html.erb |
| Помошна страница за верификација по наградна игра | app/views/public_results/verify.html.erb |
| Модел на сидро на синџирот | app/models/chain_anchor.rb |
| Обвивка за OpenTimestamps CLI | app/services/ots_client.rb |
| Часовна работа за доставување на сидро | app/jobs/anchor_audit_chain_job.rb |
| Шестчасовна работа за ажурирање на сидро (Биткоин потврда) | app/jobs/upgrade_chain_anchors_job.rb |
| Rake задача — верификација на едно извлекување | lib/tasks/draw_verify.rake |
| Rake задача — шетање на секој аудит-синџир | lib/tasks/audit_chain.rake |
| Повторувачки распоред (Solid Queue) | config/recurring.yml |
| Сервис за CSV извоз | app/services/contest_csv_export_service.rb |
| Проверка за минимум коментатори (платени наградни игри) | app/services/contest_pipeline/enforce_minimum_commenters.rb |
Оваа анализа го опишува системот заклучно со 2026-05-27. Сите тврдења се проверливи наспроти изворниот код; патеките на датотеките во §13 се авторитативната референца. Каде што овој документ и изворот се разликуваат, изворот е точен.
14. Површини за јавна верификација
Секоја URL-адреса подолу е јавна, не бара автентикација и автоматски се пополнува од системот. Креаторот на наградната игра не врши никаква дополнителна работа за да ги произведе или одржува; единствената акција на операторот што некогаш постоела е извлекувањето на наградната игра. Сè останато е автоматизација на проектот.
14.1 Страници ширум проектот (применливи за која било наградна игра)
| URL | Враќа | Публика | Видете |
|---|---|---|---|
| /fairness-analysis | Овој документ, прикажан како HTML | Секој (љубопитен, регулатор, правник) | §1–§15 |
| /fairness-analysis.pdf | Овој документ, прикажан како PDF | Ревизори кои бараат преносен артефакт | §1–§15 |
| /verify-fairness | Чекор-по-чекор водич за верификација низ шест нивоа на техничка вештина, со поимник | Учесници, новинари, набљудувачи, регулатори | §9 меѓу-референца |
14.2 Страници по наградна игра (еден сет по наградна игра, пополнет од токенот на наградната игра)
За наградна игра чија јавна URL-адреса е https://pickawinner.pro/results/<token>/<slug>:
| Шаблон на URL | Враќа | Тип на содржина | Публика | Видете |
|---|---|---|---|---|
/facebook_page_contests/<slug> |
SEO-прилагодена страница на наградна игра: иста содржина како канонската URL-адреса за резултати подолу, вклучително и лентата за верификација. Ова е URL-адресата што операторите обично ја споделуваат на социјалните мрежи | text/html | Секој (типично учесници кои пристигнуваат од социјалните мрежи) | §9.1 |
/results/<token>/<slug> |
Канонска страница за резултати со клуч-токен: идентична содржина со SEO URL-адресата погоре. Поврзана од JSON на аудит-дневникот и од каде било каде токенот е примарен идентификатор | text/html | Секој со врската (типично учесници, ревизори кои следат длабоки врски) | §9.1 |
/results/<token>/verify |
Помошник за верификација по наградна игра: ги собира значката и секоја URL-адреса подолу во една страница; меѓу-врски до /verify-fairness и /fairness-analysis | text/html | Ревизори, скептични учесници, новинари | §9.2 |
/results/<token>/eligible-ids.txt |
Канонска сортирана листа на внатрешни ID-а на секој коментар кој ги поминал сите филтри, едно ID по линија (UTF-8, прост текст) | text/plain | Секој кој извршува верификација на Ниво 2 | §9.3 |
/results/<token>/audit-log.json |
Целосен хеш-поврзан аудит-дневник по синџирски редослед; chain_status поле; anchors низа; самоопишувачки verification блок. Бајт-идентичен со slug-клуч близнакот подолу |
application/json | Секој кој извршува верификација на Ниво 3+; надворешни набљудувачи кои испитуваат за ретроактивни препишувања | §9.6 |
/facebook_page_contests/<slug>/audit-log.json |
Истиот JSON на аудит-дневник како URL-адресата со токен; поставен на slug патеката така што верификатор кој стигнал преку SEO URL-адресата за резултати може да стигне до аудит-дневникот со додавање на еден сегмент | application/json | Иста како погоре; корисно кога е во рацете само slug (пр. верификатор кој следи споделба од социјалните мрежи) | §9.6 |
/results/<token>/audit-anchors/<id>.ots |
Сирови OpenTimestamps бајти на доказ за едно сидро; верификаторот ги пренесува до ots verify |
application/vnd.opentimestamps.v1 | Секој кој извршува верификација на Ниво 5 (Биткоин) | §9.7 |
14.3 Политика за кеширање
| Крајна точка | Cache-Control | Зошто |
|---|---|---|
/fairness-analysis |
public, max-age=21600 (6 часа) |
Содржината на документот ретко се менува |
/fairness-analysis.pdf |
(без заглавие за кеш — се регенерира по барање) | PDF повторното прикажување е брзо; преферира се свежина |
/verify-fairness |
public, max-age=21600 (6 часа) |
Статична содржина |
/facebook_page_contests/<slug> |
(стандарден Rails одговор, без експлицитно заглавие за кеш) | Се ажурира кога се менува наградната игра; претежно сервира динамична содржина по гледач |
/results/<token>/<slug> |
HTTP-stale-aware преку stale?(@contest, public: true) |
Се ажурира кога се менува наградната игра |
/results/<token>/verify |
public, max-age=600 (10 минути) |
Пониско од страницата за резултати бидејќи статусот на сидрото се менува побрзо |
/results/<token>/eligible-ids.txt |
(без заглавие за кеш) | Мал товар, детерминистички, може слободно повторно да се преземе |
/results/<token>/audit-log.json |
`no-store` | Тригерот за манипулација мора да биде видлив во моментот кога ќе го извлече испитувач |
/facebook_page_contests/<slug>/audit-log.json |
`no-store` | Иста причина како близнакот со токен — идентичен сериализатор, идентични заглавија |
/results/<token>/audit-anchors/<id>.ots |
public, max-age=3600 (1 час) |
Доказите се претежно непроменливи; преминот од на чекање → надграден заслужува краток кеш |
Двете URL-адреси на страници по наградна игра сервираат идентична содржина за извлечена наградна игра, но користат различни стратегии за кеш: /results/<token>/<slug> учествува во HTTP условни GET-ови преку Rails stale?(@contest, public: true) (така што испитувачките клиенти може да добијат 304 Not Modified), додека /facebook_page_contests/<slug> секогаш повторно прикажува. Ревизори кои се грижат за свежина може да испитуваат било која; ревизори кои се грижат за минимизирање на пропусен опсег треба да ја преферираат канонската URL-адреса со клуч-токен.
14.4 Улогата на секоја URL-адреса
Секоја јавна URL-адреса постои за конкретен читател. Поделбата е намерна — верификаторите имаат уредуван еднострничен индекс, случајни посетители гледаат чисти страници за резултати, а ревизорите имаат директен програмски пристап без HTML парсирање.
- `/facebook_page_contests/<slug>` е URL-адресата што операторот обично ја споделува (Facebook објави, Instagram био, последователни коментари под оригиналната објава на наградната игра). Користи човечки-читлив slug така што добро се појавува во прегледи на врски. Значката за верификација се прикажува внатре како една-линиска лента, но никакви технички артефакти (семка, хеш, симнувања) не се прикажуваат тука. Цел: чисто, професионално, споделливо.
- `/results/<token>/<slug>` е канонската URL-адреса со клуч-токен. Идентична прикажана содржина; се користи кадешто токенот е примарен идентификатор (
proof_urlполињата на JSON на аудит-дневникот, итн.). Иста чиста лента. - `/results/<token>/verify` е уредуваниот индекс за верификатор. Ја прикажува значката повторно со експлицитно објаснување, плус семката, SHA-256, врски до симнувањето на квалификуваните ID-а, JSON на аудит-дневникот и последниот Биткоин-сидрен доказ. Меѓу-врски до чекор-по-чекор водичот и овој документ.
- Трите крајни точки
/results/<token>/{eligible-ids.txt,audit-log.json,audit-anchors/*.ots}се машински-читливи површини насочени кон програмска верификација (Ruby фрагмент,jq,ots verify). /facebook_page_contests/<slug>/audit-log.jsonе втора поставка на истиот JSON на аудит-дневникот, сервирана од истиотContestAuditChainSerializerкако близнакот со токен. Постои од една причина: верификатор кој ја знае само slug URL-адресата (бидејќи тоа е она што операторот го споделил на социјалните мрежи) може да стигне до аудит-дневникот со додавање на еден сегмент, без прво да ја отвори страницата за резултати за да го обнови токенот. Квалификуваните ID-а и доказите за сидра сè уште се само на URL-адресата со токен бидејќи не се дел од површината за откривање на манипулација по снимка и не им треба втор пат на откривање.
Оваа поделба значи дека верификатор може да запре на кој било слој:
- Поглед на значката → доверба во серверот на операторот
- Кликнување во /results/<token>/verify → потврдување преку објавените артефакти → доверба во вашиот сопствен Ruby
- Повторно пресметување на хешевите на синџирот → доверба само во SHA-256
- Верификување на Биткоин сидрото → доверба во Биткоин блокчејнот
15. Модел на закани и матрица на одбрана
Секоја чесна тврдина за правичност ги именува заканите што ги поразува и заканите што не. Матрицата подолу мапира конкретни сценарија на напад кон слојот на одбрана што ги затвора, плус преостанатиот ризик во секој ред.
| Сценарио | Слој кој го поразува | Преостанат ризик |
|---|---|---|
| Операторот намести победник преку UI на апликацијата | Не постои такво UI. Не постои оператор-контролирано копче „избери победник“ во кој било момент во контролната табла. | Ништо — нема што да се заобиколи. |
Операторот намести победник преку Rails конзолата (update!, update_columns, update_all) |
PostgreSQL BEFORE UPDATE тригер на contest_entries (§7.2) одбива секоја мутација на колоните на победник / дисквалификација кога status IN ('drawn', 'completed') |
Ништо на овој слој — тригерот се активира во базата. |
| Операторот намести победник преку сиров SQL | Истиот тригер како погоре. | Ништо. |
Оператор со PG superuser го оневозможува тригерот, ги препишува победниците, го враќа тригерот |
Операцијата се запишува во сопствениот лог на PostgreSQL за прашања; аудит-синџирот (§8) не запишува настан на извлекување за наместениот резултат, па пресметката на синџирот не открива потекло за новите победници. Биткоин сидрото (§9.7) на оригиналната глава на синџирот сè уште докажува како изгледал синџирот на наградната игра во оригиналното време на извлекувањето. | Реално, но детектабилно за секој кој држи снимка на аудит-дневникот пред препишувањето, И структурно ограничено со јавниот ангажман на Биткоин кон оригиналната глава на синџирот. |
| Операторот вметнува фалсификуван аудит-настан | before_create :assign_hash_chain го пресметува канонскиот хеш вклучително и целата претходна содржина на синџирот; ред внесен со SQL со произволна содржина има погрешен entry_hash, а previous_hash на следниот настан покажува настрана. Верификаторот (§8.4) подига ChainBroken при првото несовпаѓање. |
Ништо на криптографско ниво. Откривањето бара извршување на verify_chain! или повлекување на audit-log.json. |
| Операторот брише аудит-настан | BEFORE DELETE тригерот одбива освен ако не е поставен документираниот GUC override за каскада на родител; дури и тогаш, покажувачите на хеш на синџирот ја прокажуваат бришењето. |
Ништо за не-каскадно бришење; каскадните бришења го придружуваат уништувањето на наградната игра и не оставаат заштитен настан зад себе. |
Оператор со PG superuser ги препишува и contest_entries И contest_audit_events и ги пресметува сите низводни хешеви за да ја одржи конзистентноста на синџирот, сето во рок од ~6 часа од оригиналното извлекување (пред Биткоин да потврди) |
Слојот на апликацијата не може да го порази ова — по дефиниција е закана на администраторот на базата. Доказот на OpenTimestamps на чекање веќе не би се совпаѓал со препишаната глава на синџирот, но за време на прозорецот на чекање нападачот би можел повторно да ја достави препишаната глава на OTS наместо. | Реално но временски-ограничено на ~6 часа. Откако Биткоин ќе го потврди оригиналното сидро, препишувањето би требало и да го препише Биткоин блокчејнот. |
| Оператор со PG суперкорисник прави исто препишување >6 часа по оригиналното извлекување (Биткоин блокот веќе го потврдил оригиналното сидро) | Потврдениот блок на Биткоин го вградува дигестот на оригиналната глава на синџирот; секое препишување произведува различна глава на синџирот и не може да се совпадне со Биткоин-сидрениот доказ. Секој кој извршува ots verify врз зачуваната датотека на доказ веднаш го гледа несовпаѓањето. |
Ништо на криптографско ниво. Операторот може да одбие услуга (брише записи, одбива да го сервира JSON), но не може недетектабилно да фалсификува. |
| Операторот ги запира работата за усидрување | AdminDailyDigestJob#check_stale_anchors запишува SystemAlert(source: "audit_anchor", severity: "warning") за секоја наградна игра активна во последните 7 дена чиј последен настан е >24 часа стар без сидро. Надворешен набљудувач кој испитува audit-log.json исто така ќе види дека сидрата престануваат да се појавуваат. |
Внатрешно детектабилно во рок од 24 часа; надворешно детектабилно за секој кој го испитува JSON-от. Операторот може да престане да ги сидри идните настани, но не може ретроактивно да ги де-сидри минатите настани. |
| Операторот го брише целиот запис на наградна игра (каскадно уништување) | Каскадата го активира GUC override-от pick_a_winner.allow_audit_purge и ги отстранува аудит-настаните и сидрата на синџирот на наградната игра заедно со редот на наградната игра (§8.3, §12.6). Бајтите на .ots доказот што надворешни страни ги симнале пред уништувањето продолжуваат да се верификуваат наспроти Биткоин блокчејнот; снимките на audit-log.json земени пред уништувањето ја задржуваат целата историја на синџирот. |
Реално — затворливо само со follow-up проектот „Надворешно складиште за снимки на транскрипт“. Детектабилно надворешно од кој било набљудувач кој држи претходна снимка на audit-log.json. Операторот може да ја избрише својата сопствена копија, но не може да ги отповика снимките што други веќе ги држат. |
| Операторот го манипулира Graph API одговорот пред да влезе во базата | Никакво — API одговорот не е криптографски потпишан од Meta. Системот може да докаже од што извлекувал (секој бајт оди во draw_eligible_ids и размешувањето со семка), но не може да докаже дека тој влез се совпаѓал со она што било јавно објавено. |
Реално, структурно. Поразено само со споредување на листата на квалификувани ID-а наспроти јавно-видливата објава — што може да го направи кој било учесник. |
| Два конкурентни работници и двата се обидуваат да ја извлечат истата наградна игра | pg_try_advisory_lock(DRAW_WINNERS_NAMESPACE, contest_id); вториот работник тивко не-оп. |
Ништо. |
| Два конкурентни кодни патишта се обидуваат да запишат аудит-настани за истата наградна игра во ист миг | pg_advisory_xact_lock(AUDIT_EVENT_CHAIN, contest_id) ги серијализира вметнувањата во синџирот по наградна игра. |
Ништо. |
| Учесник коментира 1.000 пати за да добие 1.000 записи | Уникатен индекс на (facebook_page_contest_id, facebook_profile_id); upsert_all чува еден запис по профил, последниот запис победува. |
Ништо — базата спроведува еден-запис-по-лице. |
| Биткоин се-организира по нашата потврда на сидрото | По ~6 блока за потврдување (~1 час по почетната потврда), веројатноста за повторна органзиација е статистички занемарлива. | Практично нула; би го поништила не само нашиот доказ, туку и секоја друга апликација која користи OpenTimestamps усидрување во истиот прозорец. |
15.1 Скалата на доверба, во по една реченица
За верификатор кој одлучува колку длабоко да оди:
1. Погледнете ја значката — доверба во серверот на операторот. 2. Повторно извршете го размешувањето во Ruby — доверба во SHA-256 и сопствениот интерпретер. 3. Прочитајте го JSON на аудит-дневникот — иста доверба како значката, но гледате секој настан. 4. Сами повторно пресметајте ги хешевите на синџирот — доверба само во SHA-256; серверот веќе не може да скрие прекинување на синџирот. 5. Верификувајте го Биткоин сидрото — доверба во Биткоин блокчејнот. Ова е најсилното ниво на единечна-наградна-игра доверба што го нуди системот; ги поразува сите закани во матрицата погоре освен именуваните структурни (стари наградни игри, прозорец на сидро на чекање, интегритет на изворни податоци). 6. Снимајте и споредувајте во текот на времето — доверба во сопственото складирање. Открива ретроактивни препишувања дури и ако синџирот остане внатрешно конзистентен. 7. Статистичка проверка на расудувањето низ многу наградни игри — доверба во статистиката. Открива систематска пристрасност што пер-наградна-игра криптографските проверки не можат — пр. хипотетички оператор кој ја манипулирал влезната листа пред да ја хешира. Бара примерок (типично ≥ 30 наградни игри) пред тестовите да имаат значајна моќност.
Видете /verify-fairness за практичните чекори на секое ниво. Нумерацијата се совпаѓа со седумте нивоа на таа страница.
Целиот систем, во една линија: ова е гаранција дека „не можеме да предатираме или да препишеме резултати од наградна игра без вие да забележите“. Тригерите, хеш-синџирот и Биткоин сидрото не го прават операторот доверлив во некој апстрактен смисл — тие ја прават цената на недетектирано препишување еднаква на препишувањето на Биткоин блокчејнот, а цената на детектирано јавно платена пред секој учесник кој држи претходна снимка. Таа асиметрија, а не еден слој, е гаранцијата за правичност.