Розділення сховища
Інструкція split дозволяє здійснювати часткове виведення коштів з квантово-стійких сховищ шляхом розподілу лампортів між кількома рахунками. Це необхідно для схем підпису Вінтерніца, які можна безпечно використовувати лише один раз.
На відміну від традиційної криптографії, підписи Вінтерніца стають вразливими після одноразового використання. Інструкція розділення дозволяє:
Розподіляти платежі між кількома отримувачами в одній транзакції
Переносити залишкові кошти до нового квантового сховища зі свіжою парою ключів (передаючи квантове сховище як рахунок
refund)
Required Accounts
Інструкція потребує трьох рахунків:
vault: Вихідне сховище, що містить збережені лампорти (має бути змінюваним)split: Рахунок отримувача для вказаної суми (має бути змінюваним)refund: Рахунок отримувача для залишку балансу сховища (має бути змінюваним)
Рахунок refund часто є новим квантовим сховищем зі свіжою парою ключів Вінтерніца, що забезпечує постійну безпеку для залишкових коштів.
Ось як це виглядає в коді:
pub struct SplitVaultAccounts<'a> {
pub vault: &'a AccountInfo,
pub split: &'a AccountInfo,
pub refund: &'a AccountInfo,
}
impl<'a> TryFrom<&'a [AccountInfo]> for SplitVaultAccounts<'a> {
type Error = ProgramError;
fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
let [vault, split, refund] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
Ok(Self { vault, split, refund })
}
}Instruction Data
Потрібні три елементи даних:
signature: Підпис Вінтерніца, що підтверджує володіння парою ключів сховищаamount: Лампорти для переказу на розділений рахунок (8 байтів, little-endian)bump: Бамп деривації PDA для оптимізації (1 байт)
Ось як це виглядає в коді:
pub struct SplitVaultInstructionData {
pub signature: WinternitzSignature,
pub amount: [u8; 8],
pub bump: [u8; 1],
}
impl<'a> TryFrom<&'a [u8]> for SplitVaultInstructionData {
type Error = ProgramError;
fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
if data.len() != core::mem::size_of::<SplitVaultInstructionData>() {
return Err(ProgramError::InvalidInstructionData);
}
let mut signature_array = MaybeUninit::<[u8; 896]>::uninit();
unsafe {
core::ptr::copy_nonoverlapping(data[0..896].as_ptr(), signature_array.as_mut_ptr() as *mut u8, 896);
}
Ok(Self {
signature: WinternitzSignature::from(unsafe { signature_array.assume_init() }),
bump: data[896..897].try_into().map_err(|_| ProgramError::InvalidInstructionData)?,
amount: data[897..905].try_into().map_err(|_| ProgramError::InvalidInstructionData)?,
})
}
}Instruction Logic
Процес верифікації включає такі кроки:
Формування повідомлення: Створюється 72-байтове повідомлення, що містить: суму для розділення, публічний ключ рахунку
splitта публічний ключ рахункуrefundПеревірка підпису: Підпис Вінтерніца використовується для відновлення початкового хешу публічного ключа, який потім порівнюється з насінням деривації PDA сховища.
Валідація PDA: Швидка перевірка еквівалентності гарантує, що відновлений хеш відповідає PDA сховища, доводячи, що підписувач володіє сховищем.
Розподіл коштів у разі успішної валідації: вказана сума переказується на рахунок
split, залишок переказується на рахунокrefund, а рахунокvaultзакривається.
Ось як це виглядає в коді:
pub struct SplitVault<'a> {
pub accounts: SplitVaultAccounts<'a>,
pub instruction_data: SplitVaultInstructionData,
}
impl<'a> TryFrom<(&'a [u8], &'a [AccountInfo])> for SplitVault<'a> {
type Error = ProgramError;
fn try_from((data, accounts): (&'a [u8], &'a [AccountInfo])) -> Result<Self, Self::Error> {
let instruction_data = SplitVaultInstructionData::try_from(data)?;
let accounts = SplitVaultAccounts::try_from(accounts)?;
Ok(Self { accounts, instruction_data })
}
}
impl<'a> SplitVault<'a> {
pub const DISCRIMINATOR: &'a u8 = &1;
pub fn process(&self) -> ProgramResult {
// Assemble our Split message
let mut message = [0u8; 72];
message[0..8].clone_from_slice(&self.instruction_data.amount);
message[8..40].clone_from_slice(self.accounts.split.key());
message[40..].clone_from_slice(self.accounts.refund.key());
// Recover our pubkey hash from the signature
let hash = self.instruction_data.signature.recover_pubkey(&message).merklize();
// Fast PDA equivalence check
if solana_nostd_sha256::hashv(&[
hash.as_ref(),
self.instruction_data.bump.as_ref(),
crate::ID.as_ref(),
b"ProgramDerivedAddress",
])
.ne(self.accounts.vault.key())
{
return Err(ProgramError::MissingRequiredSignature);
}
// Close Vault, send split balance to Split account, refund remainder to Refund account
*self.accounts.split.try_borrow_mut_lamports()? += u64::from_le_bytes(self.instruction_data.amount);
*self.accounts.refund.try_borrow_mut_lamports()? += self.accounts.vault.lamports().saturating_sub(u64::from_le_bytes(self.instruction_data.amount));
self.accounts.vault.close()
}
}