3. Структурирование нашего аккаунта в Твиттере

Перевод | Автор оригинала: Loris

ЭПИЗОД 3

2 МЕСЯЦА НАЗАД

12 МИН ЧТЕНИЕ

Учетные записи являются строительными блоками Solana. В этом эпизоде мы объясним, что это такое и как их определить в наших программах.

Все в солане - учетная запись

В Солане все является аккаунтом.

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

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

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

Но вот где становится интереснее: даже программы являются учетными записями.

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

Так что помните, все это учетная запись в Солане. Программы, кошельки, NFT, твиты — все это сделано из учетных записей.

Simple diagram showing 3 accounts: a Tweet account, a User account and a Solana-Twitter executable account.

Определение учетной записи для наших твитов

Хорошо, давайте определим нашу первую учетную запись. Нам нужно определить структуру, которая будет содержать всю информацию, необходимую для публикации и отображения твитов.

Решение А 👎

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

Кроме того, кто-то должен платить за хранилище, которое будет существовать в блокчейне. Если нам придется предварительно выделять хранилище для каждого отдельного твита, мы в конечном итоге будем платить за все хранилище. И чем больше памяти нам потребуется, тем дороже она будет.

One "Tweet Dictionary" account with a fixed limit of tweets.

Решение Б 👍

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

Multiple "Tweet" accounts with no limit.

Реализация

Давайте реализуем решение B в нашей программе Solana. Откройте ваш файл lib.rs, в нем мы реализуем всю нашу программу. Вы можете пока проигнорировать весь существующий код и добавить следующий в конец файла.

// programs/solana-twitter/src/lib.rs

// ...

#[account]
pub struct Tweet {
    pub author: Pubkey,
    pub timestamp: i64,
    pub topic: String,
    pub content: String,
}

Вот и все! Эти 7 строк кода — все, что нам нужно для определения нашей учетной записи твита. Теперь время для некоторых пояснений.

Зачем хранить автора?

Вы можете подумать, что создание учетной записи в блокчейне Solana отслеживает ее владельца, и вы будете правы! Так зачем же нужно отслеживать автора твита внутри данных учетной записи?

Это потому, что владельцем аккаунта будет программа, создавшая его.

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

Вот небольшая диаграмма, показывающая, как все учетные записи будут связаны друг с другом.

Diagram showing that: the "System Program" executable account owns our "Solana-Twitter" executable account which itself owns 3 "Tweet" accounts. Each of these accounts stores the public key of a "User" account which are owned by the "System Program" as well.

Как видите, даже наша программа "solana-twitter" принадлежит другой учетной записи, которая является системной программой Solana. Эта исполняемая учетная запись также принадлежит каждой учетной записи пользователя. Системная программа в конечном итоге является предком всех учетных записей Solana.

Стимул хранить меньше

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

Технически нам не нужно указывать оптимальный размер для хранения наших данных. Мы могли бы просто сказать Солане, что хотим, чтобы размер наших учетных записей в Твиттере был, скажем, 4000 байт (4 КБ). Этого должно быть более чем достаточно для хранения всего нашего контента. Так почему бы и нет? Потому что Солана дает нам стимул не делать этого.

Аренда

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

Концепция проста:

Вау, чего ждать?!

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

Без арендной платы

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

Так как же создать учетную запись без арендной платы? Все просто: вам нужно добавить на счет достаточно денег, чтобы заплатить сумму, эквивалентную арендной плате за два года.

Как только вы это сделаете, деньги останутся на счете навсегда и никогда не будут получены. Еще лучше, если вы решите закрыть счет в будущем, вы получите обратно освобожденные от арендной платы деньги!

Solana предоставляет инструменты Rust, JavaScript и CLI, чтобы выяснить, сколько денег нужно добавить на счет, чтобы он был освобожден от арендной платы в зависимости от его размера. Например, запустите это в своем терминале, чтобы узнать освобожденный от арендной платы минимум для учетной записи 4 КБ.

# Ensure your local ledger is running for this to work.
solana rent 4000

# Outputs:
# Rent per byte-year: 0.00000348 SOL
# Rent per epoch: 0.000078662 SOL
# Rent-exempt minimum: 0.02873088 SOL

При этом нам не понадобятся эти методы в нашей программе, так как Anchor позаботится обо всей математике за нас. Все, что нам нужно, это выяснить, сколько памяти нам нужно для нашей учетной записи, так что давайте сделаем это сейчас.

Размер нашего аккаунта

Ранее мы определили нашу учетную запись Tweet со следующими свойствами:

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

Но сначала есть кое-что, что вы должны знать.

Дискриминатор

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

Этот дискриминатор хранит тип учетной записи. Таким образом, если у нас есть несколько типов учетных записей — скажем, учетная запись Tweet и учетная запись UserProfile — тогда наша программа может различать их.

Хорошо, давайте отследим эту информацию в нашем коде, добавив следующую константу в конец файла lib.rs.

const DISCRIMINATOR_LENGTH: usize = 8;

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

A big table where each cell is a byte. The first 8 cells are highlighted and marked as "discriminator".

Автор

Хорошо, теперь мы можем перейти к нашим собственно свойствам, заявляя с помощью открытого ключа автора.

Как узнать размер типа PubKey? Если вы используете IDE, такую как CLion, вы можете нажать, удерживая нажатой клавишу Control, тип PubKey, и он привести вас к его определению. Вот что вы должны увидеть.

pub struct Pubkey([u8; 32]);

Эта специально выглядящая структура определяет массив. Размер каждого элемента указывается в первом элементе, а длина массива — во втором элементе. Следовательно, эта структура определяет массив из 32 элементов типа u8. Тип u8 означает, что это целое число без знака из 8 бит. Поскольку в одном байте 8 бит, мы получаем общую длину массива 32 байта.

Это означает, что для хранения свойства «автор» или любого открытого ключа нам нужно всего 32 байта. Давайте также будем отслеживать эту информацию в константе.

const PUBLIC_KEY_LENGTH: usize = 32;

А вот и наше обновленное представление хранилища.

Same table as before with an additional 32 cells highlighted and marked as "author".

Отметка времени

Свойство timestamp имеет тип i64. Это означает, что это целое число из 64 бит или 8 байтов.

Давайте добавим константу, посмотрим на наше обновленное представление хранилища и перейдем к следующему свойству.

const TIMESTAMP_LENGTH: usize = 8;

Same table as before with an additional 8 cells highlighted and marked as "timestamp".

Тема

Свойство темы немного сложнее. Если вы щелкнете, удерживая клавишу Control, на типе String, вы должны увидеть следующее определение.

pub struct String {
    vec: Vec<u8>,
}

Эта структура определяет вектор (vec), содержащий элементы размером 1 байт (u8). Вектор подобен массиву, общая длина которого неизвестна. Мы всегда можем добавить в конец вектора, если у нас достаточно памяти для него.

Это все хорошо, но как мы вычислим размер его хранилища, если у него нет ограничений?

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

В нашем случае мы сохраняем тему. Это может быть: «solana», «laravel», «accessibility» и т. д.

Итак, давайте примем решение, что тема будет иметь максимальный размер 50 символов. Этого должно быть достаточно для большинства тем.

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

Оказывается, используя кодировку UTF-8, символ может занимать от 1 до 4 байт . Поскольку нам нужно максимальное количество байтов, которое может потребоваться теме, мы должны установить размер наших символов в 4 байта каждый.

Итак, мы выяснили, что наше свойство темы должно занимать не более 50 x 4 = 200 байт.

Same table as before with an additional 200 cells highlighted and marked as "topic".

Важно отметить, что этот размер является чисто ориентировочным, поскольку у векторов нет ограничений. Таким образом, пока мы выделяем 200 байт, для ввода «solana» в качестве темы потребуется всего 6 x 4 = 24 байта.

Same table as before but with 24 bytes highlighted as the topic and the rest being marked as "unused storage".

Обратите внимание, что символы в «solana» не требуют 4 байта, но я притворяюсь для простоты.

Мы почти закончили с нашим свойством темы, но осталось подумать об одном, когда речь заходит о типе String или векторах в целом.

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

Our usual byte table but with an additional 4 bytes marked as "vec prefix" added before the 200 bytes marked as "topic".

Этот префикс важен, чтобы знать, где находится следующее свойство в массиве байтов. Поскольку у векторов нет ограничений, без этого префикса мы бы не знали, где он останавливается.Same table as before but with 24 bytes instead of 200 bytes in the topic section. An arrow starting from the "vec prefix" section points to the end of the 24 bytes to illustrate that the vector prefix lets us know where the next property starts.

Фу! Хорошо, теперь, когда мы знаем, как определить размер свойств String, давайте определим несколько констант, которые суммируют наши выводы.

const STRING_LENGTH_PREFIX: usize = 4; // Stores the size of the string.
const MAX_TOPIC_LENGTH: usize = 50 * 4; // 50 chars max.

Содержание

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

Единственное, что отличается от свойства темы, это количество символов. Здесь мы хотим, чтобы содержание наших твитов было максимум 280 символов, что дает общий размер нашего контента 4 + 280 * 4 = 1124 байта.

Our usual byte table but with an additional 4 bytes marked as "vec prefix" followed by 1120 bytes marked as "content". Not all rows inside the "content" sections are shown otherwise it would be a very big table.

Как обычно, добавим для этого константу.

const MAX_CONTENT_LENGTH: usize = 280 * 4; // 280 chars max.

Итогоовый размер

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

Если кто-то захочет что-то добавить в эту таблицу, не стесняйтесь обращаться ко мне, и я обязательно буду обновлять ее.

TypeSizeExplanation
bool1 byte1 bit rounded up to 1 byte.
u8 or i81 byte
u16 or i162 bytes
u32 or i324 bytes
u64 or i648 bytes
u128 or i12816 bytes
[u16; 32]64 bytes32 items x 2 bytes. [itemSize; arrayLength]
PubKey32 bytesSame as [u8; 32]
vec<u16>Any multiple of 2 bytes + 4 bytes for the prefixNeed to allocate the maximum amount of item that could be required.
StringAny multiple of 1 byte + 4 bytes for the prefixSame as vec<u8>

Окончательный код

Давайте посмотрим на весь код, который мы написали в этой статье, и объединим наши различные константы в одну, которая даст общий размер нашей учетной записи Tweet.

// 1. Define the structure of the Tweet account.
#[account]
pub struct Tweet {
    pub author: Pubkey,
    pub timestamp: i64,
    pub topic: String,
    pub content: String,
}

// 2. Add some useful constants for sizing propeties.
const DISCRIMINATOR_LENGTH: usize = 8;
const PUBLIC_KEY_LENGTH: usize = 32;
const TIMESTAMP_LENGTH: usize = 8;
const STRING_LENGTH_PREFIX: usize = 4; // Stores the size of the string.
const MAX_TOPIC_LENGTH: usize = 50 * 4; // 50 chars max.
const MAX_CONTENT_LENGTH: usize = 280 * 4; // 280 chars max.

// 3. Add a constant on the Tweet account that provides its total size.
impl Tweet {
    const LEN: usize = DISCRIMINATOR_LENGTH
        + PUBLIC_KEY_LENGTH // Author.
        + TIMESTAMP_LENGTH // Timestamp.
        + STRING_LENGTH_PREFIX + MAX_TOPIC_LENGTH // Topic.
        + STRING_LENGTH_PREFIX + MAX_CONTENT_LENGTH; // Content.
}

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

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

И мы закончили с этим эпизодом!

Вывод

Несмотря на то, что мы написали не так много кода, мы увидели, почему и как Солана побуждает нас дважды подумать о том, какой объем памяти мы помещаем в блокчейн.

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

Как обычно, вы можете найти код этого эпизода в ветке episode-3 репозитория.

Просмотреть эпизод 3 на GitHub

Сравните с Эпизодом 2

В следующем эпизоде мы добавим больше кода в наш файл lib.rs, чтобы создать нашу первую инструкцию, которая будет отвечать за создание новой учетной записи Tweet.