Как проверить розыгрыш самостоятельно
Правильный протокол розыгрыша не просит верить сайту на слово. Результат вычисляется из двух вещей — состава участников, зафиксированного до жеребьёвки, и случайности, которую публикует независимый маяк. Обе можно взять не у нас и повторить расчёт.
Из чего складывается результат
отпечаток анонса = SHA-256(состав участников + параметры + номер раунда маяка)
seed = SHA-256(отпечаток анонса : случайность раунда)
результат = детерминированный алгоритм(seed)
Ключевой момент — порядок событий. Анонс публикуется раньше, чем выходит назначенный раунд маяка, поэтому в момент фиксации списка случайность не известна никому: ни организатору, ни нам. А когда раунд вышел, результат уже предопределён — выбора не остаётся.
Шаг 1. Убедитесь, что анонс был раньше случайности
На странице протокола есть ссылка на анонс со временем публикации и номером раунда drand.
Время раунда считается по формуле 1692803367 + (номер − 1) × 3 секунд от
начала эпохи Unix — это открытые параметры цепочки quicknet. Если анонс опубликован раньше
этого момента, подогнать список под известную случайность было невозможно.
Шаг 2. Возьмите случайность у drand напрямую
drand — сеть маяка случайности League of Entropy (Cloudflare, EPFL, Kudelski Security и другие). Каждые три секунды сеть публикует новое значение, подписанное пороговой подписью участников: подделать или заранее вычислить его в одиночку нельзя. Откройте раунд по прямой ссылке с протокола:
https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/<номер раунда>
Значение randomness в ответе должно совпасть с указанным в протоколе.
Шаг 3. Пересчитайте результат
Прямо на странице протокола есть кнопка «Проверить независимо» — она делает это в вашем
браузере и запрашивает drand напрямую. Если не доверяете нашему скрипту, вот тот же расчёт
на Python без сторонних библиотек. Запуск:
python3 proverka.py https://pro-chislo.ru/rozygrysh/<код>
import hashlib, json, sys, urllib.request
url = sys.argv[1].rstrip('/')
d = json.load(urllib.request.urlopen(url + '/json'))
# 1. отпечаток анонса: состав участников и параметры, зафиксированные до жеребьёвки
commit_json = json.dumps({
'type': d['type'],
'title': d['title'],
'items': d['items'],
'params': d['params'],
'beacon': {'chain': d['beacon']['chain'], 'round': d['beacon']['round']},
}, ensure_ascii=False, separators=(',', ':'))
commit_hash = hashlib.sha256(commit_json.encode()).hexdigest()
print('отпечаток анонса:', commit_hash, 'совпал' if commit_hash == d['commit_hash'] else 'НЕ СОВПАЛ')
# 2. случайность берём у самого drand, а не у сайта
beacon_url = 'https://api.drand.sh/%s/public/%d' % (d['beacon']['chain'], d['beacon']['round'])
randomness = json.load(urllib.request.urlopen(beacon_url))['randomness']
print('случайность маяка:', 'совпала' if randomness == d['beacon']['randomness'] else 'НЕ СОВПАЛА')
# 3. seed и детерминированный генератор
seed = hashlib.sha256((commit_hash + ':' + randomness).encode()).hexdigest()
print('seed:', seed, 'совпал' if seed == d['seed'] else 'НЕ СОВПАЛ')
class Prng:
def __init__(self, seed):
self.seed, self.counter, self.buf = seed, 0, b''
def uint32(self):
while len(self.buf) < 4:
block = '%s:%d' % (self.seed, self.counter)
self.buf += hashlib.sha256(block.encode()).digest()
self.counter += 1
v, self.buf = int.from_bytes(self.buf[:4], 'big'), self.buf[4:]
return v
def below(self, n):
if n <= 1:
return 0
limit = (2 ** 32 // n) * n # хвост отбрасываем: иначе перекос к малым числам
while True:
v = self.uint32()
if v < limit:
return v % n
def between(self, a, b):
return a + self.below(b - a + 1)
def draw(kind, items, p, seed):
r = Prng(seed)
if kind == 'range':
excluded, used, out = set(p.get('exclude') or []), set(), []
while len(out) < p['count']:
n = r.between(p['from'], p['to'])
if n in excluded or (p['uniq'] and n in used):
continue
used.add(n)
out.append(n)
if p.get('sort'):
out.sort()
return {'winners': [str(n) for n in out]}
if kind == 'teams':
pool = list(items)
for i in range(len(pool) - 1, 0, -1):
j = r.below(i + 1)
pool[i], pool[j] = pool[j], pool[i]
teams = [[] for _ in range(p['teams'])]
for i, name in enumerate(pool):
teams[i % p['teams']].append(name)
return {'teams': teams}
pool, picked = list(items), []
for i in range(p['count']):
if p['uniq']:
j = i + r.below(len(pool) - i)
pool[i], pool[j] = pool[j], pool[i]
picked.append(pool[i])
else:
picked.append(pool[r.between(0, len(pool) - 1)])
return {'winners': picked}
result = draw(d['type'], d['items'], d['params'], seed)
print('результат:', 'СОВПАЛ' if result == d['result'] else 'НЕ СОВПАЛ')
print(result)Шаг 4. Проверьте, что запись не переписали
Каждый протокол включён в цепочку: звено считается как
SHA-256(предыдущее звено : отпечаток протокола) и публикуется в
открытом журнале без раскрытия участников.
Сохраните номер и хеш своего звена — если запись когда-нибудь изменят, разойдутся все
последующие звенья, которые к тому моменту уже видели другие люди.
Шаг 5. Метка времени независимой службы
Звено цепочки штампуется службой меток времени по стандарту RFC 3161 (freetsa.org): она подписывает «такой-то хеш существовал в такой-то момент» своим сертификатом. Это внешнее закрепление — оно не зависит ни от нашего сервера, ни от нашей базы. Скачайте токен со страницы протокола и проверьте локально:
# сертификаты службы (один раз)
wget https://freetsa.org/files/tsa.crt https://freetsa.org/files/cacert.pem
# что подписано и когда
openssl ts -reply -in rozygrysh-КОД.tsr -text
# сверка с хешем звена, указанным в протоколе
openssl ts -verify -digest <chain_hash> -in rozygrysh-КОД.tsr \
-CAfile cacert.pem -untrusted tsa.crt
Ответ Verification: OK означает, что именно этот хеш был предъявлен службе
в указанное время.
Чего проверка не покажет
Математика доказывает, что результат честно получен из того списка, который был зафиксирован. Она ничего не говорит о том, полон ли этот список: внёс ли организатор всех участников конкурса и не добавил ли «своих». Единственная защита здесь — опубликовать ссылку на анонс до розыгрыша, чтобы участники успели найти себя в списке. Для розыгрышей по номерам то же касается нумерации: её нужно публиковать вместе с анонсом.
Провести проверяемый розыгрыш Открытый журнал
Если вы организатор и вопрос стоит шире проверки — правила конкурса, сбор участников, оформление итогов и налог на приз разобраны в гиде как провести розыгрыш.
Отзывы и комментарии
Войдите, чтобы оставить отзыв или комментарий
Пока нет комментариев. Будьте первым!