Модели транзакций: UTXO против аккаунтов
Состояние это «кто чем владеет». Но представить владение можно двумя разными способами, и выбор тянет за собой всё: как считать баланс, как ловить двойную трату, насколько легко распараллелить проверку. Разберём обе модели, выберем одну и закодируем её так, чтобы двойную трату нельзя было провернуть.
Идея
Главный враг любых денег это двойная трата: потратить одну монету дважды. С физическими купюрами это невозможно (отдал бумажку, её больше нет), а байты копируются даром. Вся бухгалтерия блокчейна существует ради одного: сделать двойную трату невыразимой. Есть два классических способа вести эту бухгалтерию.
UTXO: монета это непотраченный выход
Модель Bitcoin. Тут нет поля «баланс». Есть множество UTXO, неделимых порций монет, каждая привязана к владельцу. Тратя, ты ссылаешься на свои UTXO как на входы и создаёшь новые выходы. Входы исчезают (помечаются потраченными), выходы становятся новыми UTXO.
/// Ссылка на конкретный выход прошлой транзакции (что тратим).
pub struct OutPoint {
pub txid: Hash, // в какой транзакции
pub index: u32, // какой по счёту её выход
}
/// Выход: сколько монет и кому (упрощённо, адресом).
pub struct TxOutput {
pub amount: u64,
pub owner: Address,
}
/// UTXO-транзакция: тратит перечисленные входы, создаёт новые выходы.
pub struct UtxoTx {
pub inputs: Vec<OutPoint>, // какие UTXO тратим (и подписи к ним)
pub outputs: Vec<TxOutput>, // какие создаём
}
Двойную трату тут ловит само устройство: UTXO можно потратить ровно один раз, потому что после траты он исчезает из множества непотраченных. Сослался на уже потраченный выход, транзакция невалидна. Баланс не хранится, а считается: сумма твоих UTXO. Бонус в приватности и в параллелизме: транзакции, тратящие разные UTXO, не конфликтуют и проверяются независимо (на этом стоит Solana, увидишь в уроке про её архитектуру). Цена: кошельку приходится складывать сумму из кусочков, как наличку в кармане, и следить за «сдачей».
Аккаунты: у адреса есть баланс и nonce
Модель Ethereum, и наш выбор. У каждого адреса есть поле «баланс» и счётчик nonce. Транзакция просто двигает число с одного счёта на другой, как банковский перевод. Это ближе к интуиции и проще моделировать типами.
Но тут двойная трата так просто не ловится: ничто не «исчезает». Поэтому от неё защищает nonce, порядковый номер транзакции отправителя. Транзакция валидна, только если её nonce равен текущему счётчику аккаунта. Прошла, счётчик увеличился на единицу.
/// Транзакция в аккаунтной модели: перевод amount с from на to,
/// плюс комиссия fee майнеру.
pub struct Transaction {
pub from: Address,
pub to: Address,
pub amount: u64,
pub fee: u64,
pub nonce: u64,
pub public_key: PublicKey,
pub signature: Signature,
}
Повтор той же транзакции несёт устаревший nonce (он уже пройден), и сеть его отвергает. Заодно nonce жёстко задаёт порядок транзакций одного адреса: пятая не применится раньше четвёртой. Двойная трата превращается в «послать два разных перевода с одним nonce», но в блок попадёт только первый: после него счётчик уже сдвинется, и второй станет невалиден.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.