Чтения с доказательством кошелька
Почти каждое чтение на интерфейсе JSON-RPC узла открыто, потому что то, что оно возвращает, уже реплицировано на каждый узел. Горстка — нет, и линия между ними не «секретно ли это» — здесь ничто не секретно — а «собирает ли ответ на это незнакомцу нечто, что протокол намеренно оставляет разрозненным».
Защищаемая вещь — граф сделок
Конечная точка, которая отвечает с кем этот кошелёк торгует и как часто, вручает любому карту реальных торговых отношений: к какому мерчанту кошелёк всегда возвращается, кто постоянные клиенты занятого мерчанта, и, следовательно, за кем стоит проследить до дома. На одноранговом фиатном рынке это вопрос физической безопасности, а не предпочтение.
Этот аргумент был приведён однажды и применён в одном методе (getCounterparties),
пока тот же граф оставался доступен через getSettlements, getReservations и
getDisputes — ни один из которых не принимал параметр, и все возвращали каждую
запись сети с обеими сторонами, названными и с ключами. Ворота не были слабы; их
обошли. Это были также три метода, а не один: резервирование называет покупателя,
а его объявление называет мерчанта, поэтому то же ребро было доступно на шаг
раньше, включая сделки, которые так и не были рассчитаны.
Поэтому публичные чтения теперь отредактированы, а сторона читает свои собственные записи полностью, доказывая, что владеет кошельком.
Рукопожатие
В этом протоколе нет аккаунтов, поэтому на «это правда вы?» можно ответить только попросив вызывающего подписать нечто, что он не мог бы подписать заранее.
1. Запросите nonce
{ "method": "getWalletChallenge", "params": { "wallet": "<base64 PeerId>" } }
{ "result": { "subject": "<base64 PeerId>", "nonce": "<64 hex chars>", "expires_at": 1753800300000 } }
Выдача намеренно открыта. nonce ничего не стоит без приватного ключа, который его подписывает, а требовать подпись для получения того, что вы подписываете, было бы циклично; ответ ничего не подтверждает о кошельке, даже что он существует. Вызовы живут только в памяти и истекают через пять минут — достаточно долго, чтобы человек прочитал и одобрил запрос кошелька, достаточно коротко, чтобы неистраченный nonce не валялся.
getCounterpartiesChallenge и getProviderEarningsChallenge — тот же эмитент под
разными именами. Один nonce отвечает ровно на один вызов, на какой бы поверхности
он ни был истрачен.
2. Подпишите вызов
Байты для подписи — это строка UTF-8:
<domain>:<subject>:<nonce>
subject — это канонический peer id в base64, с которым вернулся вызов, а не то
написание base64, которое вы отправили. domain фиксирован для каждого метода:
| Метод | Домен |
|---|---|
getMySettlements | openfiat-my-settlements |
getMyReservations | openfiat-my-reservations |
getMyDisputes | openfiat-my-disputes |
getCounterparties | openfiat-counterparties |
getProviderEarnings | openfiat-earnings |
Разделитель домена — причина того, что подпись, собранная для одной поверхности с воротами, не может быть предъявлена на другой, даже если обе идентифицируют свой субъект одинаково и черпают nonce из одного реестра.
3. Вызовите метод
{
"method": "getMySettlements",
"params": {
"wallet": "<base64 PeerId>",
"public_key": "<base64 raw 32-byte Ed25519 public key>",
"nonce": "<the nonce from step 1>",
"signature": "<base64 64-byte Ed25519 signature>"
}
}
getMyReservations, getMyDisputes и getCounterparties принимают ровно те же
четыре поля. Публичный ключ отправляется явно, а не восстанавливается из wallet,
поэтому заявление об идентичности — это то, что вызывающий утверждает, а узел
проверяет, а не то, что узел выводит за вызывающего, — он должен вывестись ровно к
тому кошельку, о котором спрашивают.
То, что возвращается, не отредактировано, и только для записей, стороной которых
является кошелёк. getMyDisputes также отвечает севшему арбитру, потому что
читать всё дело — его работа.
Детали, которые важны, если вы это реализуете
Отказ, а не сужение. Вызывающий, который не может доказать кошелёк, получает ошибку, никогда отфильтрованный ответ. Реализация с фильтрацией выглядит идентично в каждом проходящем тесте, ровно до тех пор, пока рефакторинг не уберёт фильтр; отказ падает громко и немедленно.
Порядок проверок. Проверка вывода ключа происходит первой, до того как nonce затронут, чтобы неудачная попытка незнакомца не могла истратить nonce, который его настоящий владелец на середине подписания. Затем nonce потребляется до того как подпись проверена, чтобы предъявление перехваченной подписи сжигало nonce вместо его повтора.
«Неизвестно» и «уже истрачено» — одна и та же ошибка. Их различение подтвердило бы, что какая-то другая сторона в середине рукопожатия по этому субъекту.
Ничего нового не хранится. Незакрытые вызовы в памяти, а ответы сворачиваются по требованию из записей, которые узел уже реплицирует. Оператор узла не получает никакой записи о том, кто что спрашивал, — что важно, потому что оператор — ровно та сторона, для которой это не должно тихо строить досье.