Фрагменты и квитанции
Сообщение никогда не передаётся целиком. Оно делится на фрагменты, которые идут через разные узлы, и каждый доставленный фрагмент порождает анонимную квитанцию: доказательство того, что работа выполнена, без указания, для кого.
Рид–Соломон 3 из 5
Каждый дополненный конверт делится на полосы k = 3 из n = 5 кодом Рида–Соломона над GF(256): из пяти частей хватит любых трёх. Два узла могут пропасть, а сообщение всё равно дойдёт.
| Схема | Объём | Вероятность доставки, если каждый маршрут срабатывает в 95 % случаев |
|---|---|---|
| 4 из 4, без избыточности | 1,00× | 81,5 % |
| 4 + 1 чётность (v1) | 1,25× | 97,7 % |
| 3 из 5 (v2) | 1,67× | 99,9 % |
Размер части зависит от блока: треть блока с округлением до 1 КБ, но не больше 24 КБ. Большие конверты используют несколько полос.
Shard = { id, msg, mailbox, stripe, idx, k, n, bucket, data, rh, exp }
id = sha512(msg ‖ stripe ‖ idx)[0..32] msg случаен при каждой отправке
Фрагмент не содержит ни реального размера сообщения (он внутри конверта, в зашифрованном виде), ни каких-либо сведений об отправителе.
Сменяемые почтовые ящики
Фрагменты ждут в почтовом ящике. У каждого устройства своё семя ящика, а сам ящик меняется каждый день:
ящик(день) = hex(HMAC-SHA512(mailboxSeed, "day:" + floor(ts / 86 400 000)))[0..40]
Отправитель использует сегодняшний ящик получателя; получатель открывает сегодняшний и вчерашний. Узел не может связать ящик понедельника с ящиком вторника.
Открыть ящик может только владелец
Открытие ящика не подписывается личностью. Отправитель добавляет в каждую часть метку mbAuth, выведенную из секрета ящика получателя. Чтобы открыть ящик, владелец предъявляет доказательство, подписанное над velum/v2/open|<ящик>|<ts>, и узел сверяет его с этой меткой. Так свои части забирает только владелец, а узел не узнаёт, кто он.
Доказательство доставки: анонимная квитанция
Внутри конверта (который может открыть только получатель) лежит случайное семя rseed. Для каждой части:
секрет_i = HMAC-SHA512(rseed, shardId)[0..32]
rh_i = SHA-512(секрет_i) ← идёт в части, у всех на виду
Собрав сообщение, получатель раскрывает каждый секрет_i. Любой может проверить, что SHA-512(секрет) == rh, но никто другой не мог знать секрет: узел не способен подделать квитанцию.
{
"shard": "7a0c…", // shard id
"mailbox": "c2e81f…", // today's rotating mailbox (40 hex)
"rh": "…128 hex…", // sha512(secret), already inside the shard
"secret": "…64 hex…", // HMAC-SHA512(rseed, shardId)[0..32], only the recipient can compute it
"carrier": "vel…", // node that accepted the shard
"courier": "vel…", // node that delivered it
"ts": 1793577600000
}
В квитанции нет ни подписи, ни адреса получателя. Каждая квитанция даёт баллы:
- 1 балл carrier (узлу, принявшему часть).
- 1 балл courier (узлу, доставившему её).
- Если это один и тот же узел, 2 балла.
От квитанций к заявкам
При миллиардах сообщений в день квитанции не могут попадать в блоки по одной. Каждый узел собирает свои в дерево Меркла (SHA-512) и публикует только корень и итог (work.claim). В следующую эпоху маяк выбирает 16 случайных квитанций, и узел должен предъявить их вместе с секретом и путём в дереве (work.prove). Если хотя бы одна не сходится, узел теряет награду за эпоху. Подробнее — в разделе Запусти узел.
Защита от обмана
- Одна и та же часть учитывается только один раз.
- Лимит баллов за эпоху для пары (узел, ящик): 400.
- Квитанции, отличающиеся от времени блока больше чем на ±15 минут, не учитываются.
- Никто не может заявить больше чем в 1,5 раза сверх ожидаемого для его веса в жеребьёвке.
- Выдумывать трафик невыгодно: чтобы получить квитанции, нужно открыть конверты, а это может сделать только настоящий получатель.
- Узлы, которые несут каждую часть, определяются публичной жеребьёвкой из 3 кандидатов; клиент лишь выбирает ближайший из трёх.
Удаление
Как только узел видит действительную квитанцию на часть, он её удаляет. Квитанция расходится по сети, и все узлы, у которых была копия, тоже её удаляют. Если часть никто не забрал, она уничтожается через 7 дней.