Rust
Pinocchio Quantum Vault

Pinocchio Quantum Vault

7 Graduates

Split Vault

The split instruction enables partial withdrawals from quantum-resistant vaults by distributing lamports across multiple accounts. This is essential for Winternitz signature schemes, which can only be used once securely.

Unlike traditional cryptography, Winternitz signatures become vulnerable after a single use. The split instruction allows you to:

  • Distribute payments across multiple recipients in one transaction
  • Roll over remaining funds to a new quantum vault with fresh keypair (by passing in a quantum vault as the refund account)

Required Accounts

The instruction requires three accounts:

  • vault: Source vault containing stored lamports (must be mutable)
  • split: Recipient account for the specified amount (must be mutable)
  • refund: Recipient account for remaining vault balance (must be mutable)

The refund account is often a new quantum vault with a fresh Winternitz keypair, ensuring continued security for the remaining funds.

Here's how it looks in code:

rust
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 })
    }
}

Account validation is handled by the runtime. If accounts don't meet requirements (mutability), the instruction will fail automatically.

Instruction Data

Three pieces of data are required:

  • signature: Winternitz signature proving ownership of the vault's keypair
  • amount: Lamports to transfer to the split account (8 bytes, little-endian)
  • bump: PDA derivation bump for optimization (1 byte)

Here's how it looks in code:

rust
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

The verification process follows these steps:

  1. Message Assembly: A 72-byte message is constructed containing: Amount to split, the split account publickey and the refund account publickey

  2. Signature Verification: The Winternitz signature is used to recover the original public key hash, which is then compared against the vault's PDA derivation seeds.

  3. PDA Validation: A fast equivalence check ensures the recovered hash matches the vault's PDA, proving the signer owns the vault.

  4. Fund Distribution If validation succeeds: the specified amount is transferred to the split account, the remaining balance is transferred to the refund account and the vault acount is closed.

Since the program owns the vault account, you can transfer lamports directly without having to call a Cross-Program Invocation (CPI).

Here's how it looks in code:

rust
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()
    }
}

Reconstructing the message prevents signature replay attacks where malicious actors substitute different recipient accounts while MEV-ing valid signatures captured from the mempool.

Next PageClose Vault
OR SKIP TO THE CHALLENGE
Ready to take the challenge?
Contents
View Source
Blueshift © 2025Commit: de6a6b9