Архитектура Solana: параллелизм из объявленных аккаунтов
Мы полтора блока строили и разбирали машины, которые исполняют транзакции строго по очереди: собственный узел в уроке 12, EVM в уроке 26. Одна транзакция закончилась, началась следующая. Solana задаёт другой вопрос: а почему, собственно, по очереди? Если две транзакции трогают разные счета, их можно посчитать разом, на разных ядрах. Всё устройство Solana это ответ на этот вопрос, и оно держится на одном требовании к транзакции. Разберём его.
Идея: код и данные разведены
Первое, что надо принять, идёт вразрез с Ethereum. Там контракт это код плюс его хранилище в одном флаконе: у контракта ERC-20 внутри лежит mapping с балансами, и адрес контракта это адрес и программы, и данных (урок 25).
В Solana это разъято. Программа это только код, у неё нет своего изменяемого состояния. Всё состояние живёт в отдельных аккаунтах, а программа получает их на вход и правит их байты. Аккаунт это ячейка с четырьмя полями: баланс в lamports, поле owner (какая программа вправе менять данные), флаг executable и байтовый буфер data.
Правило одно и жёсткое: менять data аккаунта может только программа, записанная в его owner. Твой кошелёк принадлежит системной программе, токен-аккаунт принадлежит токен-программе, аккаунт с состоянием игры принадлежит программе игры. Это буквально владелец файла в файловой системе, только на цепи.
Зачем такое разделение? Ради второго, главного.
Sealevel: параллелизм, который видно заранее
Вернёмся к вопросу из вступления. Чтобы исполнять транзакции параллельно, узлу нужно знать наперёд, кто с кем конфликтует. EVM этого знать не может: контракт во время работы вправе тронуть любое хранилище, поэтому узел вынужден гнать всё по очереди, иначе два потока подрались бы за одну ячейку.
Solana выворачивает это требование наружу. Sealevel заставляет каждую транзакцию заранее объявить все аккаунты, которые она тронет, и пометить, какие только читает, а какие пишет. Раз списки известны до запуска, планировщик берёт старое правило из мира баз данных: две транзакции конфликтуют, только если одна пишет туда, куда другая пишет или читает. Не конфликтуют, значит их можно исполнить разом.
Это ровно те же грабли, что у блокировок в своей СУБД (урок про конкурентность) и у &/&mut в Rust: сколько угодно читателей совместимы, но писатель эксклюзивен. Правило конфликта пишется в одну функцию:
/// Заявка транзакции: какие аккаунты она читает и какие пишет.
pub struct Locks {
pub reads: Vec<AccountId>,
pub writes: Vec<AccountId>,
}
impl Locks {
/// Конфликт есть, если запись одной пересекается с чтениями или записями
/// другой. Чтение с чтением совместимо, всё остальное с записью нет.
pub fn conflicts_with(&self, other: &Locks) -> bool {
let touches = |w: &[AccountId], other: &Locks| {
w.iter().any(|a| other.writes.contains(a) || other.reads.contains(a))
};
touches(&self.writes, other) || touches(&other.writes, self)
}
}
Дальше планировщик жадно раскладывает пачку по волнам: каждую транзакцию кладёт в первую волну, с которой у неё нет конфликта, а если такой не нашлось, открывает новую. Внутри волны всё исполняется параллельно, волны идут по очереди.
pub fn schedule(txs: &[Locks]) -> Vec<Vec<usize>> {
let mut waves: Vec<Vec<usize>> = Vec::new();
'tx: for (i, tx) in txs.iter().enumerate() {
for wave in waves.iter_mut() {
if wave.iter().all(|&j| !tx.conflicts_with(&txs[j])) {
wave.push(i);
continue 'tx;
}
}
waves.push(vec![i]);
}
waves
}
Настоящий планировщик Solana умнее (учитывает комиссии и плотнее набивает ядра), но правило конфликта у него ровно это. Покрути пачку транзакций и посмотри, как меняются волны, когда счета пересекаются:
none → r → w → none. Транзакция объявляет, что читает и пишет.5 транзакций разложились в 2 волны. Строго по очереди это 5 шагов; Sealevel исполняет каждую волну разом на разных ядрах, потому что её аккаунты не пересекаются. Сделай так, чтобы все писали в один аккаунт, и параллелизм схлопнется: запись эксклюзивна, как &mut в Rust. Оставь всем чтение одного аккаунта, и они снова сольются в одну волну: читатели друг другу не мешают.
Заметь связь с разделом про Go (горутины и каналы): там мы руками разводили доступ к общим данным, чтобы горутины не подрались. Sealevel делает то же самое, но на уровне протокола и без блокировок в коде: конфликты вычислены до запуска, а не разрулены во время.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.