Назад к блогу

Spritely: децентрализованный интернет на Scheme, WebAssembly и CRDT

Spritely: децентрализованный интернет на Scheme, WebAssembly и CRDT

Проект Spritely предлагает радикально иной взгляд на устройство сети: вместо централизованных серверов с таблицами прав доступа — объектно-возможностная модель, в которой доступ определяется самой ссылкой на объект. Статья разбирает, из каких частей складывается эта архитектура — рантайм Goblins, протокол OCapN, компилятор Scheme в WebAssembly и CRDT — и почему делегирование полномочий без администратора может оказаться безопаснее привычных ACL.

Spritely — это попытка собрать сетевые приложения так, чтобы их не нужно было централизованно хостить. Вопрос, на который отвечает проект, звучит так: можно ли построить систему, где доступ определяется наличием ссылки на объект, а не записью в таблице прав на сервере, и где код исполняется в браузере, а не на чужой машине? Ответ собирается из нескольких независимых частей: модели безопасности на возможностях, распределённого объектного рантайма Goblins, сетевого протокола OCapN, компилятора Scheme в WebAssembly и системы локальных имён. Ниже — механика каждой части.

Почему централизация деградирует

Авторы связывают ухудшение централизованных платформ с социально-экономическими силами. Сначала приходят венчурные деньги, и «никто не беспокоится»; затем инвесторы спрашивают, когда деньги вернутся, и именно тогда «включаются ручки эншитификации» — вплоть до исчезновения сервисов.

Техническая сторона централизации в том, что её легко снабдить бэкдором, а у пользователей мало выбора и возможностей, и они могут не знать о происходящем.

Отдельно авторы выделяют «законодательные рвы» (legislative moats) — непреднамеренный эффект законов, задуманных против крупных игроков. Такой закон создаёт правовую среду, в которой играть способны только крупные игроки: он блокирует мелких участников и решения, что опасно для самостоятельного хостинга независимых проектов. Люди боятся хостить существующие инструменты из-за появляющихся законов.

Ответ авторов — система, которую никто не хостит или, в некотором смысле, хостят все: тогда меняется сам вопрос о том, кого преследовать. Пример такой системы — Brassica Chat, объединяющий CRDT с peer-to-peer и средой capability security. Авторы подчёркивают, что помимо масштабирования вверх нужны масштабирование вниз (чтобы хостить на малых ресурсах) и вширь (развёртывание не-экспертами на множестве сред).

ACL против объектно-возможностной модели

В ACL-модели предоставление привилегий всегда требует централизации authority. Причина простая: в ACL-системе нет безопасного способа делегирования. Чтобы выдать кому-то привилегию, нужно обратиться к команде администраторов. Если у вас есть доступ к базе данных и вы хотите дать Кристине доступ только для чтения, но вы не администратор — передать ей это полномочие нельзя.

Делегирование в ACL-системе существует, но оно небезопасно, не поддаётся аудиту и не позволяет призвать к ответственности за злоупотребление: люди делятся паролями, и в этом нет ни безопасности, ни аудита, ни средств удержания accountable. Дополнительно ACL-системы подвержены ambient authority: программы и операционные системы запускаются со всеми привилегиями запустившего их пользователя. Отсюда риск confused deputy.

Разница видна на примере программы Solitaire. В ACL-модели она запускается «как Alisha» и может делать всё, что может Alisha, включая много опасных вещей, которые Alisha не хотела бы: читать письма, отправлять банковские данные, удалять или шифровать файлы. В объектно-возможностной модели действует принцип наименьших полномочий: программы, объекты и процедуры определяются в среде без опасной власти, и Solitaire может работать только с той властью, которая ей передана. Запущенная как процедура, она не может ничего опасного — но и ничего полезного, даже вывести изображение на экран.

Чтобы игра заработала, ей явно передают возможность, создающую окно с холстом, который читает ввод с клавиатуры и мыши только пока окно активно:

solitaireWin = makeWinCanvas("Safe Solitaire")
solitaire(solitaireWin)

Аналогично доступ к файлу рекордов выдаётся отдельной процедурой: файл открывается и передаётся третьим аргументом в solitaire. Без передачи этого значения объект не получит authority на файл, потому что в ocap-системе authority не подразумевается, а только явно передаётся — «если у тебя этого нет, ты не можешь это использовать».

Goblins: vat, ход и транзакции

Vat — базовая единица исполнения в Goblins. Внутри vat три компонента: actormap, stack и queue. Actormap — это хранилище объектов, принадлежащих данному vat: именно потому, что все они лежат в одной куче, они «близки» друг другу и могут вызываться напрямую. Stack хранит синхронные сообщения — обычные вызовы «близких» объектов. Queue хранит асинхронные сообщения — как между «близкими» объектами, так и из других vat'ов (от «далёких» объектов).

Синхронные операции выполняются оператором $: он записывается как ($ объект 'метод аргументы) и возвращает немедленный результат. Асинхронные операции выполняются оператором <-: он записывается как (<- объект 'метод аргументы), применяется к объекту, живущему где угодно, и вместо немедленного результата возвращает обещание.

Сообщения обрабатываются по одному ходу за раз, как ходы игроков за столом. Сообщение, адресованное конкретному объекту, передаётся текущему поведению получателя. Получатель может вызывать другие «близкие» объекты синхронно, менять своё состояние или поведение, порождать новые объекты, вызывать обычные выражения хост-языка или отправлять асинхронные сообщения — последние уходят только если ход успешно завершён.

Транзакционность встроена в сам ход. Синхронные вызовы дают дешёвую транзакционность: если внутри обработчика возникает ошибка, изменения в actormap откатываются. В примере обработчик сначала увеличивает счётчик times-called, затем вызывает ошибку — и после этого чтение счётчика возвращает 0, то есть состояние восстановлено к значению до вызова. Необработанные ошибки приводят к откату хода: он просто никогда не коммитится в корневую транзакционную кучу, что предотвращает случайную порчу данных.

Синхронные и асинхронные вызовы, promise pipelining

Синхронный вызов $ работает только если оба объекта определены на одной машине и в одном цикле событий, и возвращает немедленный результат. Асинхронный <- можно применять к объектам, живущим где угодно, включая удалённые машины, но вместо результата он возвращает promise.

Promise pipelining даёт сразу две вещи: удобный интерфейс для описания цепочки асинхронных действий — можно вызывать объекты, на которые указывают обещания, ещё до их разрешения, иногда до существования самих объектов, — и сетевую абстракцию, устраняющую множество round-trip'ов. Без pipelining поток сообщений между машинами B и A выглядит так:

B => A => B => A => B

С pipelining тот же поток сокращается до B => A => B. Экономия достигается за счёт того, что сообщение адресуется обещанию напрямую, без отдельного on для последующей отправки.

Ошибки по цепочке тоже распространяются. Если фабрика сломана и создание машины завершается ошибкой, обещание машины оказывается нарушенным. У конвейерного сообщения к нарушенному обещанию нет места, куда доставить сообщение, — это тоже трактуется как сбой. Исключение из нарушенного обещания распространяется на следующее в цепочке, так что ситуацию можно обнаружить и обработать.

Time-travel debugging и персистентность

Отладка с путешествием во времени опирается на транзакционную природу ходов: каждый ход — транзакция, и при необработанной ошибке он не коммитится. Сериализуется граф объектов: сериализатор начинает с корневых объектов и вызывает у каждого специальный метод, запрашивая «автопортрет». Объекты могут давать автопортрет только в пределах уже имеющихся у них прав, а сам механизм запечатан — распечатать его может только сериализатор. Поскольку обход всего графа дорог, сериализация может использовать информацию о дельтах транзакций хода, чтобы сериализовать только изменившиеся объекты. Восстановление идёт обратным обходом графа с применением каждого автопортрета к его рецепту построения — и это удобный момент для запуска кода обновления; Goblins будет собирать шаблоны обновления в общую библиотеку.

Ограничение подхода в том, что он опирается на транзакционную кучу и на невозможность выдать себе полномочия, которых не было: например, созданные игроками объекты не должны получать или раздавать игровую валюту или давать себе силы, которых у них изначально не было. Распределённый отладчик с трассировкой сообщений, вдохновлённый Causeway, ещё только планируется; в сочетании с time travel он позволит отлаживать программу в состоянии ошибки в момент её возникновения. Сериализованный граф можно использовать и для визуализации сохранённой ocap-системы, чтобы программист видел граф полномочий.

Именование: petnames и треугольник Зуко

Petnames решают проблему, описанную треугольником Зуко: имена могут быть либо человеко-значимыми, либо децентрализованными, либо безопасными, но не всеми тремя. Tor onion-адреса децентрализованы и безопасны — идентификатор это Base32-кодированный криптографический публичный ключ, — но не человекочитаемы. Petnames накладываются поверх таких имён, чтобы получить человеко-значимость, как список контактов в телефоне, где «mom» означает именно мою маму, а не глобальную.

Кроме petnames есть proposed names вроде «Hi, I'm Dave»; они не глобально уникальны, поэтому возможны коллизии. Авторы считают petnames необходимыми для безопасности, потому что глобальные системы именования страдают от фишинга, захвата доменов и тайпсквоттинга.

Sealers и unsealers: инкапсуляция прав

make-sealer-triplet возвращает три значения: sealer, unsealer и brand-check. Sealer и unsealer — это парные роли: sealer прячет значение внутрь запечатанного объекта, а unsealer — единственный способ достать это значение обратно. Brand-check нужен на практике, чтобы отличить «свой» запечатанный объект от чужого: он подтверждает, что объект запечатан именно нужным sealer'ом, и работает как аналог проверки подписи. При каждом вызове определяется новый тип <seal>, полностью отличный от типов, созданных другими вызовами, поэтому brand-check одного триплета не признаёт объекты, запечатанные sealer'ом другого, а без соответствующего unsealer'а содержимое недоступно.

На этом строится контроль доступа к блогу. Автор поста получает доступ к редактору через метод, возвращающий редактор, запечатанный sealer'ом блога. Администратор владеет unsealer'ом блога и потому может распечатать этот объект и передать аргументы в редактор. При добавлении поста администратор проверяет self-proof: пост возвращает запечатанный self-proof, администратор распечатывает его и сверяет идентичность объекта. Если кто-то без доступа к unsealer'у — например, Mallet со своими собственными sealer и unsealer — попытается подсунуть пост, распечатывание выбросит исключение с сообщением, что self-proof не для этого поста. Mallet может создать свои sealer и unsealer, но они не соответствуют sealer и unsealer администратора, поэтому не могут быть использованы ни для чего опасного: нужного sealer'а у Mallet нет, значит воспользоваться им нельзя.

Revocation и accountability

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

Логирование действий администратора устроено через объект-логгер, хранящий список записей. Каждая запись содержит имя пользователя, объект, к которому было обращение, и переданные аргументы. Прокси-объект перед выполнением вызова проверяет, не отозван ли доступ, и если нет — сначала записывает вызов в лог, а затем перенаправляет его объекту. Lauren может обнаружить несанкционированное изменение, проверив лог: она замечает, что её пост назван не так, как она помнила, и смотрит записи. Это связано с accountability: Lauren хочет иметь возможность призвать к ответственности любого, кому она дала доступ, за принимаемые ими решения, и при необходимости отозвать этот доступ.

OCapN: сетевой слой

Netlayer может быть построен на разных транспортных слоях — от peer-to-peer сетей до клиент-серверных архитектур. CapTP располагается поверх абстрактного интерфейса netlayers, что позволяет устанавливать защищённые соединения между двумя сторонами. Абстракция обеспечивает независимость от транспортного протокола: поддерживаются Tor Onion Services, I2P, libp2p, DNS + TLS и даже шифрованные sneakernets. Есть и временная абстракция соединения: поддерживаются как живые сессии для высокопроизводительных сокетных соединений, так и системы с высокой задержкой и периодическим офлайн/онлайн store-and-forward. Совместимость между реализациями обеспечивается тем, что протокол общий и может широко реализовываться на разных языках программирования.

CapTP даёт единообразную передачу сообщений без различия между локальными и удалёнными объектами. Доверие устанавливается через netlayer; вход в сеть и идентификация расположения объектов выполняются через унификацию URI-схем, а сертификаты дают меньшую простоту при обмене, но и меньшую уязвимость к утечке. CapTP также включает распределённую сборку мусора (серверы могут совместно освобождать ненужные ресурсы) и promise pipelining. OCapN в целом — это объединённые слоистые абстракции CapTP, netlayers и специфичных для OCapN URI. Протокол способен работать без центральных властей даже в одноранговых сетях с враждебными участниками.

Hoot: Scheme в WebAssembly

Hoot включает ассемблер, дизассемблер и даже интерпретатор WebAssembly для тестирования программ вне браузера. Это компилятор целых программ, стремящийся создавать минимально возможные бинарники для отправки пользователям, и он использует новейшие стандарты WebAssembly, в частности набор операций WebAssembly GC. Hoot поддерживает все основные браузеры и среды выполнения JavaScript, такие как Node.js, благодаря чему инструменты Spritely могут работать в браузере и охватывать более широкую аудиторию. Hoot также служит основой для реализации других языков, компилируемых в WebAssembly, особенно динамических или использующих сборку мусора.

Авторы сознательно выбрали «странный язык» для исследований, а не JavaScript, и «пошли ва-банк» на WebAssembly, поставляя все программы как WebAssembly-бинарники. Выбор Scheme обоснован минимализмом и гибкостью, которые позволяют чисто выражать ключевые идеи Goblins, а также гибкостью, быстрой итерацией и расширяемостью языка. Авторы ожидают работу и в средах на базе V8 вроде Bun и Deno.

Портативное шифрованное хранилище

Требования к хранилищу такие. Документы должны быть content addressed и location agnostic: обычно имя — это хеш документа для immutable-документов и публичный ключ (или его хеш) для mutable-документов. Поддерживаются оба вида, причём mutable обычно строятся поверх immutable. Документы должны быть зашифрованы так, чтобы храниться в местах, не осведомлённых об их содержимом, и только обладающие read capabilities могли получать доступ к содержимому. Документы должны быть разбиты на чанки, чтобы не быть уязвимыми к size-of-file attacks. Чтение (а для mutable-документов и запись) должно осуществляться через абстрактные capabilities. Файлы должны быть network agnostic: peer-to-peer, client-to-server и sneakernet сети должны поддерживаться с одними и теми же object URIs.

IPFS этим требованиям не удовлетворяет, поскольку не обеспечивает приватность и шифрование из перечисленного, хотя и может использоваться как основа, на которой эти слои строятся.

Практические примеры: блог с разграничением прав

В примере с блогом административный интерфейс напрямую участвует в создании новых постов и редакторов. Функция spawn-adminable-post-and-editor принимает admin и аргументы, вызывает у admin метод 'new-post-and-editor, получает пару (post editor) и возвращает их как два значения. Каждый участник-администратор может редактировать любой пост через свой admin-объект, не отслеживая отдельный editor для каждого поста. При попытке редактирования без прав, как в случае Лорен и поста Роберта, редактирование невозможно, поскольку Роберт не поделился с Лорен возможностью редактирования.

Для гостевого поста с рецензированием функция создаёт пост и три объекта-возможности: сам пост, редактора и рецензента. Автор получает restricted-editor, который позволяет менять только заголовок и тело, но не имя автора, и только пока пост не отправлен. Рецензент получает reviewer с методом approve, который добавляет пост в блог и помечает его как отправленный. Обе роли защищены проверкой, которая выбрасывает ошибку при попытке изменить или повторно одобрить уже отправленный пост. Публикация возможна только через вызов approve у reviewer, а restricted-editor такой возможности не имеет, поэтому без одобрения рецензента пост не публикуется.

Сравнение с Solid

Spritely и Solid связаны через ActivityPub: Solid был у истоков рабочей группы W3C Social Web, где стандартизировали ActivityPub, и у систем есть совместимость — обе используют один и тот же Linked Data inbox. Solid работает в мире Linked Data и RDF, который авторы Spritely считают очень интересным и недостаточно исследованным, и они обменивались идеями с людьми из Solid.

Различие в фокусе: Solid не всегда так же сосредоточен на переносимости данных, тогда как Spritely ставит во главу угла вопрос, что происходит, когда сервер падает. Spritely делает акцент на peer-to-peer сценариях, выживаемости контента и автономии пользователя, а также на локально-ориентированном подходе, интегрируя CRDT с peer-to-peer и средой capability security. Авторы считают свой подход перспективнее, потому что он не ориентирован на хайп и максимизацию прибыли, а нацелен на реальные потребности людей, и потому что предлагает масштабирование не только вверх, но и вниз и вширь, чтобы системы могли разворачивать неспециалисты.

Без криптовалют

Spritely не использует криптовалюты: технология построена на криптографии, а не на криптовалюте. Безопасность и децентрализация обеспечиваются объектно-ориентированной моделью распределённых объектов и мандатной безопасностью, где доступ определяется наличием ссылки: если у вас нет ссылки, вы не можете использовать объект. Для распределённых систем используется OCapN, работающий без центральных властей даже в одноранговых сетях с враждебными участниками. Транспортный уровень CapTP обеспечивает асинхронную передачу сообщений без различия между локальными и удалёнными объектами, а netlayers поддерживают различные транспортные протоколы, включая Tor Onion Services, I2P, libp2p, DNS+TLS и даже зашифрованные sneakernets.

Что из этого следует на практике

  • Права передаются ссылкой, а не выдаются администратором. В ocap-модели не нужно обращаться к команде администраторов, чтобы дать доступ: достаточно передать capability. Это убирает центральную точку, через которую проходят все выдачи прав, и вместе с ней — риск, что эта точка станет целью атаки.
  • Отзыв и аудит встраиваются в саму передачу. Пара revocable proxy позволяет отозвать доступ установкой флага, а прокси-логгер записывает имя пользователя, объект и аргументы до перенаправления вызова. Accountability не требует отдельной инфраструктуры — она часть механизма доступа.
  • Транзакционность хода даёт откат при ошибке бесплатно. Поскольку каждый ход — транзакция, необработанная ошибка просто не коммитится, и состояние actormap остаётся прежним. На этом же свойстве строится отладка с путешествием во времени.
  • Promise pipelining сокращает сетевые задержки. Сообщение адресуется обещанию напрямую, без ожидания его разрешения, поэтому цепочка из нескольких обращений экономит round-trip'ы, а исключение распространяется по цепочке, так что сбой можно обнаружить и обработать.
  • Транспорт не зашит в протокол. Netlayers позволяют одной и той же объектной модели работать поверх Tor, I2P, libp2p, DNS+TLS и sneakernet, а CapTP скрывает разницу между локальными и удалёнными объектами.
  • Имена остаются локальными. Petnames дают человекочитаемость поверх криптографически безопасных имён, не требуя глобальной уникальности, — а значит, не воспроизводят фишинг, захват доменов и тайпсквоттинг глобальных систем именования.
  • Код исполняется в браузере. Hoot компилирует Scheme в WebAssembly и поддерживает основные браузеры и Node.js, что позволяет доставлять программы как бинарники, а не как серверные сервисы.

Источники

Похожее