Фрагменты и квитанции

Сообщение никогда не передаётся целиком. Оно делится на фрагменты, которые идут через разные узлы, и каждый доставленный фрагмент порождает анонимную квитанцию: доказательство того, что работа выполнена, без указания, для кого.

Рид–Соломон 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 дней.

Весь путь целиком