Как проверить розыгрыш самостоятельно

Правильный протокол розыгрыша не просит верить сайту на слово. Результат вычисляется из двух вещей — состава участников, зафиксированного до жеребьёвки, и случайности, которую публикует независимый маяк. Обе можно взять не у нас и повторить расчёт.

Из чего складывается результат

отпечаток анонса = 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 означает, что именно этот хеш был предъявлен службе в указанное время.

Чего проверка не покажет

Математика доказывает, что результат честно получен из того списка, который был зафиксирован. Она ничего не говорит о том, полон ли этот список: внёс ли организатор всех участников конкурса и не добавил ли «своих». Единственная защита здесь — опубликовать ссылку на анонс до розыгрыша, чтобы участники успели найти себя в списке. Для розыгрышей по номерам то же касается нумерации: её нужно публиковать вместе с анонсом.

Провести проверяемый розыгрыш Открытый журнал

Если вы организатор и вопрос стоит шире проверки — правила конкурса, сбор участников, оформление итогов и налог на приз разобраны в гиде как провести розыгрыш.

Отзывы и комментарии

Войдите, чтобы оставить отзыв или комментарий

Я Войти через Яндекс VK Войти через VK

Пока нет комментариев. Будьте первым!