Создание децентрализованного музыкального плеера на IPFS +27



В этой статье описаны результаты двухмесячных экспериментов с IPFS. Главным итогом этих экспериментов стало создание proof-of-concept стримингового аудио плеера, способного формировать фонотеку исключительно на основе информации, публикуемой в распределённой сети IPFS, начиная с метаданных (название альбома, треклист, обложка), заканчивая непосредственно аудио-файлами.


Таким образом, будучи десктопным electron-приложением, плеер не зависит ни от одного централизованного ресурса.


IPFS


Как это работает


На первый взгляд технология напоминает собой что-то среднее между BitTorrent, DC, git и blockchain. На второй выясняется, что IPFS может хранить и распространять объекты любого вышеупомянутого протокола.


Начнём с того, что IPFS — это одноранговая сеть компьютеров, которые обмениваются данными. Для её формирования используется комбинация различных сетевых технологий, развивающаяся в рамках отдельного проекта libp2p. На каждом компьютере имеется специальный кэш, где до поры до времени хранятся данные, которые пользователь публиковал либо качал. Информация в кэше IPFS хранится в виде блоков и по умолчанию не закреплена. Как только места станет не хватать, местный garbage collector подчистит блоки, к которым давно не обращались. Методы pin / unpin отвечают за то, чтобы закрепить / открепить информацию в кэше.


Для каждого блока рассчитывается уникальный криптографический отпечаток его содержимого, который потом выступает в качестве ссылки на это содержимое в IPFS-пространстве. Блоки могут объединяться в merkle-tree объекты, узлы которых обладают собственным отпечатком, рассчитанным на основе отпечатков нижних узлов / блоков. Тут и начинается самое любопытное.


IPLD-DAG


IPFS состоит из большого количества более мелких проектов Protocol Labs, один из которых носит название IPLD (InterPlanetary Linked Data). Не уверен, что смогу грамотно объяснить суть, так что любопытным оставлю ссылку на спецификацию.


Если коротко, то описанная в предыдущем параграфе архитектура настолько гибкая, что на её основе можно воссоздавать более сложные структуры, такие как блокчейн, unix-подобная файловая система или репозиторий git. Для этого используется соответствующие интерфейсы / плагины.


Я пока работал исключительно с dag-интерфейсом, позволяющим публиковать в IPFS обычные JSON-объекты:


const obj = {
  simple: 'object'
}

const dagParams = { format: 'dag-cbor', hashAlg: 'sha3-512' }

const cid = await ipfs.dag.put(obj, dagParams)

В случае успеха возвращается объект контент-идентификатора (CID), внутри которого хранится уникальный отпечаток опубликованной информации, а также методы его конвертации в различные форматы. Вот таким образом можно получить base-строку.


const cidString = cid.toBaseEncodedString()
console.log(cidString) // zdpuAzE1oAAMpsfdoexcJv6PmL9UhE8nddUYGU32R98tzV5fv

Эта строка может использоваться для получения данных обратно при помощи метода get. При этом никаких дополнительных параметров уже не требуется, так как в строке содержится информация о формате данных и использованном алгоритме шифрования:


const obj = await ipfs.dag.get(cidString)
console.log(obj.value)
// {
//   simple: 'object'
// }

Дальше — больше. Можно опубликовать второй объект, одно из полей которого будет ссылаться на первый.


const obj2 = {
  complex: 'object',
  link: {
    '/' : cidString // cid-строка первого объекта
  }
}
const cid2 = await ipfs.dag.put(obj2, dagParams)
const cid2String = cid2.toBaseEncodedString()

При попытке получить obj2 обратно будут разрешены все внутренние ссылки, если специальным параметром не будет указано обратное.


const obj2 = await ipfs.dag.get(cid2String)
console.log(obj2.value)
// {
//   complex: "object",
//   link: {
//     "simple" : "object"
//   }
// }

Можно затребовать значение отдельного поля, для чего нужно к CID-строке добавить постфикс вида /название_поля.


const result = await ipfs.dag.get(cid2String+"/complex")
console.log(result.value)
// object

Таким образом можно "ходить" по всему объекту, в том числе по его ссылкам на другие объекты как если бы они были одним большим объектом. Так как ссылки являются криптографическими хешами содержимого, объект не может содержать ссылку на самого себя.


Концепция IPLD предполагает возможность ссылаться не только на другие DAG-объекты, но и любые другие IPLD-структуры. Таким образом, одно поле вашего объекта может ссылаться на git-коммит, а другое на биткоин-транзакцию.

Pub/Sub


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



const topic = 'межпространственное кабельное'

const receiveMsg = (msg) => {
// новое сообщение в виде буфера
}

await ipfs.pubsub.subscribe(topic, receiveMsg)

const msg = new Buffer('новость')

await ipfs.pubsub.publish(topic, msg)

Одним из первых приложений, основанных на pubsub, был irc-подобный p2p-чат orbit (до сих пор работает, если что). Подписываешься на канал, получаешь сообщения, история хранится между участниками и постепенно "забывается". Такой Vypress для всего Интернета.


И в go и в js версии IPFS pubsub-интерфейс включается специальным параметром, так как пока считается экспериментальной технологией.

Задумка


В какой-то момент своей аудиофильской жизни я заметил, что уже давно слушаю музыку исключительно при помощи Google Music. Причём не только на телефоне, но уже и на десктопе. Причин несколько, но главная — ощутимо меньшее количество кликов между "хочу послушать" и "слушаю" в большинстве повседневных ситуаций в сравнении с теми же торрентами. Вторая по важности причина — размер доступной фонотеки. Очень редко я не нахожу то, что ищу (гораздо чаще я встречаю региональные ограничения). Но это уже преимущество не перед торрентами, а другими стриминговыми сервисами.



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


  • возможность напрямую отслеживать появление данных в распределённой сети,
  • публикуемые участниками сети данные должны быть стандартизированы.

В BitTorrent-экосистеме когда кому-то захочется что-то скачать ему в большинстве случаев придётся сначала обратиться к торрент-каталогу — обычному сайту, расположенному на обычном сервере за обычным ip под обычным доменом. Первое условие необходимо для того, чтобы приложение могло формировать локальную базу данных автоматически и без участия централизованных ресурсов.


Второе условие наиболее важное. Для его выполнения необходимо, чтобы публикуемая участниками распределённой сети информация оформлялась в соответствии с заранее известной всем остальным моделью / схемой. Это делает распределённое пространство данных доступным для систем поиска и фильтрации, повышает их результативность.


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


Реализация


На сегодня "Патефон" представляет собой примитивную плеер-фонотеку. Вот практически вся его функциональность:


  • публикация альбомов в IPFS
  • список обнаруженных альбомов
  • возможность проиграть альбом

Начнём с публикации. В предыдущей главе я говорил о том, что информация в распределённой сети должна быть каким-то образом стандартизирована. В "Патефоне" эта задача достигается за счёт json-схемы альбома.


const albumSchema = {
  type: "object",
  properties: {
    title: {
      type: "string"
    },
    artist: {
      type: "string"
    },
    cover: {
      type: "string"
    },
    tracks: {
      type: "array"
      items: {
        type: "object",
        properties: {
          title: {
            type: "string"
          },
          artist: {
            type: "string"
          },
          hash: {
            type: "string"
          }
        }
      }
    }
  }
}

В соответствии с этой схемой конкретный экземпляр проверяется, а затем и публикуется. За это отвечает обычная форма ввода.



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



Важная деталь — прикреплённые файлы публикуются отдельно при помощи ipfs.files.add(). Вернувшиеся CID’ы вставляются в соответствующий инпут отдельной строкой. Инпут оставлен на случай, если файла под рукой нет, но известен его CID.

Когда форма заполнена и пользователь жмёт "ADD ALBUM" итоговые JSON-данные сперва проверяются на соответствие схеме, затем публикуются при помощи DAG-интерфейса, после чего полученный CID в виде буфера рассылается в специальной pubsub-комнате.



const isValid = validateAlbumObj(albumObj) // проверка данных формы на соответствие схеме

if (isValid) {
  const albumCid = await ipfs.dag.put(albumObj, dagParams) // публикация объекта альбома при помощи dag
  await ipfs.pubsub.publish(albumSchemaCidString, albumCid.buffer) // рассылка CID альбома в pubsub в виде буфера
}

Очевидным решением было бы использовать в качестве ключа комнаты что-то вроде "albums", "music-albums" или, в крайнем случае, "pathephone", если хочется изолировать сеть в рамках своего приложения. Но потом в голову пришла более умная мысль — использовать в качестве ключа CID-строку json-схемы альбома. В конце концов именно cхема данных по-настоящему объединяет все копии приложения. Более того, именно от этой комнаты можно ожидать, что все принимаемые сообщения будут содержать проверенные экземпляры этой схемы.



const handleReceivedMessage = async (message) => {
  try {
    const { data, from } = message // data - буфер полученных данных, from - id отправителя
    const cid = new CID(data).toBaseEncodedString() // предполагая, что полученный буфер является CID-отпечатком, создаём на его основе CID-объект и сразу же конвертируем в base-строку
    const { value } = await ipfs.dag.get(cid)
    const isValid = validateAlbumObj(value) // проверяем полученный объект на соответствие схеме альбома
    if (isValid) {
      // value - проверенный экземпляр альбома, делаем с ним всё, что душе угодно
    } else {
      throw new Error('invalid schema instance received')
    }
  } catch (error) {
    // обработка исключений
  }
}

const albumSchemaCid = await ipfs.dag.put(albumSchema, dagParams)
const albumSchemaCidString = albumSchemaCid.toBaseEncodedString()
await ipfs.pubsub.subscribe(
  albumSchemaCidString, handleReceivedMessage
)

Как видите, каждый "прилетающий" объект всё равно проверяется на соответствие схеме, так как никто не мешает любому участнику сети, зная ключ, целенаправленно начать флудить в комнате. В то же время уникальность ключа сводит к минимуму вероятность случайного вмешательства. С другой стороны, если какое-нибудь другое приложение начнёт пользоваться той же схемой, что и "Патефон", то они вместе начнут формировать и использовать одно и то же пространство данных, что может пойти в плюс.


За скобками остались некоторые специфичные для приложения вещи, вроде сохранения объекта в базе данных.


Процесс публикации альбома на одном компьютере и получения его на другом можно посмотреть на видео. Cкорость практически моментальная, но это зависит от большого количества факторов. Был случай, когда компьютеры так и не оказались в одном "рое", так что передачу совершить не удалось.

Metabin Gate


Всё описанное выше я оформил в виде npm-модуля под названием @metabin/gate. Он прячет детали, оставляя разработчику лишь передать ipfs-ноду и json-схему:



const gate = await openGate(
  ipfsNode, // запущенный экземпляр js-ipfs или js-ipfs-api
  schema // валидная json-schema,
  (instance, cid) => {
    // instance - проверенный экземпляр указанной схемы
    // cid - base-encoded отпечаток экземпляра
  }
)

await gate.send(instance) // публикация экземпляра

await gate.close()

Выводы


IPFS — одна из самых продвинутых технологий распределённого обмена информацией. Несмотря на то, что обе (и go и javascript) версии находятся в стадии альфы, ими уже пользуются некоторые относительно популярные приложения, вроде OpenBazaar или d.tube. Другое дело, что используют его в основном только в качестве файлового хранилища.


Это можно понять, учитывая что IPFS в основном и рассматривается как своеобразная альтернатива BitTorrent. Концепция IPLD и возможности соответствующих интерфейсов редко оказываются в центре внимания. Хотя, на мой взгляд, это самая перспективная разработка Protocol Labs, дающая возможность распространять через IPFS обычные объекты данных.


А совместив нехитрым образом files, dag и pubsub-интерфейсы разработчик получает бесплатный, самоорганизующийся источник данных для своего приложения. Добавьте Electron и получите довольно заманчивый стек технологий:


  • не требует наличия серверов
  • не требует наличия доменного имени
  • скорость распространения данных ограничена только количеством и качеством участников
  • сложность и структура данных ограничена только вашей фантазией
  • не надо заморачиваться с кроссбраузерной поддержкой
  • высокая устойчивость к цензуре

Само собой, не всем приложениям такая автономность вообще нужна, учитывая что она ведёт к потере контроля над контентом. Не говоря уже о том, что всё это довольно экспериментальная область. Как такая сеть будет масштабироваться, как это скажется на производительности, можно ли на этот процесс повлиять — вопросов много, ещё больше не сформулировано.


Тем не менее возможности и перспективы открываются любопытные.


Ссылки





К сожалению, не доступен сервер mySQL