Split Vault
Die split Anweisung ermöglicht teilweise Abhebungen aus quantenresistenten Tresoren durch Verteilung von Lamports auf mehrere Konten. Dies ist für Winternitz-Signaturverfahren unerlässlich, die aus Sicherheitsgründen nur einmal verwendet werden können.
Im Gegensatz zur traditionellen Kryptographie werden Winternitz-Signaturen nach einmaliger Verwendung angreifbar. Die Split-Anweisung ermöglicht es dir:
Zahlungen in einer Transaktion auf mehrere Empfänger zu verteilen
Verbleibende Mittel in einen neuen Quantum-Tresor mit frischem Schlüsselpaar zu übertragen (indem ein Quantum-Tresor als
refundKonto übergeben wird)
Required Accounts
Die Anweisung erfordert drei Konten:
vault: Quell-Tresor mit gespeicherten Lamports (muss veränderbar sein)split: Empfängerkonto für den angegebenen Betrag (muss veränderbar sein)refund: Empfängerkonto für das verbleibende Tresorguthaben (muss veränderbar sein)
Das refund Konto ist oft ein neuer Quantum-Tresor mit einem frischen Winternitz-Schlüsselpaar, um die kontinuierliche Sicherheit der verbleibenden Mittel zu gewährleisten.
So sieht es im Code aus:
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
Drei Datenelemente sind erforderlich:
signature: Winternitz-Signatur, die den Besitz des Schlüsselpaars des Tresors nachweistamount: Zu übertragende Lamports auf das Split-Konto (8 Bytes, Little-Endian)bump: PDA-Ableitungs-Bump zur Optimierung (1 Byte)
So sieht es im Code aus:
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
Der Verifizierungsprozess folgt diesen Schritten:
Nachrichtenzusammenstellung: Eine 72-Byte-Nachricht wird erstellt, die Folgendes enthält: Zu splittender Betrag, den öffentlichen Schlüssel des
splitKontos und den öffentlichen Schlüssel desrefundKontosSignaturüberprüfung: Die Winternitz-Signatur wird verwendet, um den ursprünglichen Public-Key-Hash wiederherzustellen, der dann mit den PDA-Ableitungssamen des Tresors verglichen wird.
PDA-Validierung: Eine schnelle Äquivalenzprüfung stellt sicher, dass der wiederhergestellte Hash mit dem PDA des Tresors übereinstimmt und beweist damit, dass der Unterzeichner Eigentümer des Tresors ist.
Fondsverteilung bei erfolgreicher Validierung: Der angegebene Betrag wird auf das
splitKonto überwiesen, das verbleibende Guthaben wird auf dasrefundKonto überwiesen und dasvaultKonto wird geschlossen.
So sieht es im Code aus:
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()
}
}