#![allow(deprecated, unexpected_cfgs)] //! TARDIGRADE //! //! Post quantum vaults on Solana. A vault is a program derived address whose //! seed is the root of a Winternitz one time public key. Nothing that relies on //! an elliptic curve can move funds out of it: the only way out is a Winternitz //! signature, which is nothing but SHA-256 hash chains. //! //! Parameters //! digest 28 bytes (SHA-256 truncated), 224 bits, about 112 bits post quantum //! chains 28 message chains + 2 checksum chains = 30 //! steps 255 per chain (base 256 digits) //! step h' = trunc28( sha256( [chain, step] || h ) ) //! root sha256( pk_0 || pk_1 || ... || pk_29 ) //! vault PDA [ "vault", root ] //! //! A spend is a sweep: `amount` goes to the recipient and everything left goes //! to a fresh vault (`refund`) under a new key, so a key is only ever used once. //! The signature (840 bytes) does not fit next to the accounts of a token sweep //! in one transaction, so it travels in two halves: //! stage writes the message and the first 15 chains into a stage account //! execute carries the last 15 chains, verifies all 30, moves the funds and //! closes the stage account //! //! Instructions //! 0 stage [payer s w, stage w, vault, system] //! tag, vault_bump, stage_bump, mint[32], recipient[32], //! refund[32], amount u64, sig[0..420] //! 1 execute_sol [payer s w, stage w, vault w, recipient w, refund w, //! system, spent w] //! tag, sig[420..840] //! 2 execute_token [payer s w, stage w, vault, vault_ata w, recipient, //! recipient_ata w, refund, refund_ata w, mint, //! token_program, ata_program, system, spent w] //! tag, sig[420..840] //! 3 deposit_sol [payer s w, vault w, system, spent] tag, amount u64 //! 4 deposit_token [payer s w, payer_ata w, vault, vault_ata w, mint, //! token_program, ata_program, system, spent] //! tag, amount u64 //! 5 cancel [payer s w, stage w] tag //! //! The mint field is all zeros for a SOL vault. use solana_program::{ account_info::{next_account_info, AccountInfo}, entrypoint::ProgramResult, hash::hashv, instruction::{AccountMeta, Instruction}, msg, program::{invoke, invoke_signed}, program_error::ProgramError, pubkey::Pubkey, rent::Rent, system_instruction, system_program, sysvar::Sysvar, }; #[cfg(not(feature = "no-entrypoint"))] solana_program::entrypoint!(process_instruction); pub const HASH_LEN: usize = 28; pub const MSG_CHAINS: usize = 28; pub const CHAINS: usize = 30; pub const STEPS: usize = 255; pub const SIG_LEN: usize = CHAINS * HASH_LEN; // 840 pub const HALF: usize = SIG_LEN / 2; // 420 pub const DOMAIN: &[u8] = b"TARDIGRADE/v1"; pub const TOKEN_PROGRAM: Pubkey = solana_program::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); pub const TOKEN_2022_PROGRAM: Pubkey = solana_program::pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); pub const ATA_PROGRAM: Pubkey = solana_program::pubkey!("ATokenGPvbd2Na2EpG2WNoM8tNVmYZn1qy6S9k4Fbo6D"); const SPENT_TAG: u8 = 0x53; const SPENT_LEN: usize = 1; // stage account layout const ST_TAG: usize = 0; // 1 byte, 0x54 const ST_VAULT: usize = 1; // 32 const ST_PAYER: usize = 33; // 32 const ST_VBUMP: usize = 65; // 1 const ST_MINT: usize = 66; // 32 const ST_RECIP: usize = 98; // 32 const ST_REFUND: usize = 130; // 32 const ST_AMOUNT: usize = 162; // 8 const ST_SIG: usize = 170; // 420 pub const STAGE_LEN: usize = ST_SIG + HALF; // 590 const STAGE_TAG: u8 = 0x54; #[derive(Debug)] #[repr(u32)] pub enum VaultError { BadInstruction = 0x7a00, BadSignature, WrongVault, WrongStage, WrongAccount, NotEnough, WrongTokenProgram, SameVault, AlreadySpent, } impl From for ProgramError { fn from(e: VaultError) -> Self { ProgramError::Custom(e as u32) } } // ───────────────────────── Winternitz ───────────────────────── #[inline(always)] fn step(chain: u8, s: u8, h: &[u8; HASH_LEN]) -> [u8; HASH_LEN] { let out = hashv(&[&[chain, s], h]).to_bytes(); let mut r = [0u8; HASH_LEN]; r.copy_from_slice(&out[..HASH_LEN]); r } /// The 28 byte message digest a spend signs. pub fn digest(program_id: &Pubkey, vault: &Pubkey, mint: &[u8; 32], recipient: &[u8; 32], refund: &[u8; 32], amount: u64) -> [u8; HASH_LEN] { let out = hashv(&[DOMAIN, program_id.as_ref(), vault.as_ref(), mint, recipient, refund, &amount.to_le_bytes()]).to_bytes(); let mut r = [0u8; HASH_LEN]; r.copy_from_slice(&out[..HASH_LEN]); r } /// The 30 digits: 28 message bytes then a 2 byte big endian checksum. pub fn digits(d: &[u8; HASH_LEN]) -> [u8; CHAINS] { let mut out = [0u8; CHAINS]; let mut c: u32 = 0; for i in 0..MSG_CHAINS { out[i] = d[i]; c += (STEPS as u32) - d[i] as u32; } out[28] = (c >> 8) as u8; out[29] = (c & 0xff) as u8; out } /// Walks every chain from the signature to its end and returns the root. pub fn recover_root(sig: &[u8], dg: &[u8; HASH_LEN]) -> [u8; 32] { let ds = digits(dg); let mut pks = [0u8; SIG_LEN]; for i in 0..CHAINS { let mut h = [0u8; HASH_LEN]; h.copy_from_slice(&sig[i * HASH_LEN..(i + 1) * HASH_LEN]); let mut s = ds[i] as usize; while s < STEPS { h = step(i as u8, s as u8, &h); s += 1; } pks[i * HASH_LEN..(i + 1) * HASH_LEN].copy_from_slice(&h); } hashv(&[&pks]).to_bytes() } /// Off chain helpers, used by tests and to produce vectors for the browser. pub fn public_root(secrets: &[[u8; HASH_LEN]; CHAINS]) -> [u8; 32] { let mut pks = [0u8; SIG_LEN]; for i in 0..CHAINS { let mut h = secrets[i]; for s in 0..STEPS { h = step(i as u8, s as u8, &h); } pks[i * HASH_LEN..(i + 1) * HASH_LEN].copy_from_slice(&h); } hashv(&[&pks]).to_bytes() } pub fn sign(secrets: &[[u8; HASH_LEN]; CHAINS], dg: &[u8; HASH_LEN]) -> [u8; SIG_LEN] { let ds = digits(dg); let mut sig = [0u8; SIG_LEN]; for i in 0..CHAINS { let mut h = secrets[i]; for s in 0..ds[i] as usize { h = step(i as u8, s as u8, &h); } sig[i * HASH_LEN..(i + 1) * HASH_LEN].copy_from_slice(&h); } sig } // ───────────────────────── helpers ───────────────────────── fn read32(d: &[u8], o: usize) -> Result<[u8; 32], ProgramError> { d.get(o..o + 32).and_then(|s| s.try_into().ok()).ok_or_else(|| VaultError::BadInstruction.into()) } fn read_u64(d: &[u8], o: usize) -> Result { d.get(o..o + 8).and_then(|s| s.try_into().ok()).map(u64::from_le_bytes).ok_or_else(|| VaultError::BadInstruction.into()) } fn key_is(a: &AccountInfo, k: &Pubkey) -> ProgramResult { if a.key != k { return Err(VaultError::WrongAccount.into()); } Ok(()) } fn signer(a: &AccountInfo) -> ProgramResult { if !a.is_signer { return Err(ProgramError::MissingRequiredSignature); } Ok(()) } fn token_program_ok(a: &AccountInfo) -> ProgramResult { if *a.key != TOKEN_PROGRAM && *a.key != TOKEN_2022_PROGRAM { return Err(VaultError::WrongTokenProgram.into()); } Ok(()) } fn ata_create_idempotent<'a>(payer: &AccountInfo<'a>, ata: &AccountInfo<'a>, owner: &AccountInfo<'a>, mint: &AccountInfo<'a>, system: &AccountInfo<'a>, token: &AccountInfo<'a>, ata_prog: &AccountInfo<'a>) -> ProgramResult { let ix = Instruction { program_id: ATA_PROGRAM, accounts: vec![ AccountMeta::new(*payer.key, true), AccountMeta::new(*ata.key, false), AccountMeta::new_readonly(*owner.key, false), AccountMeta::new_readonly(*mint.key, false), AccountMeta::new_readonly(system_program::ID, false), AccountMeta::new_readonly(*token.key, false), ], data: vec![1], }; invoke(&ix, &[payer.clone(), ata.clone(), owner.clone(), mint.clone(), system.clone(), token.clone(), ata_prog.clone()]) } fn transfer_checked_ix(token: &Pubkey, from: &Pubkey, mint: &Pubkey, to: &Pubkey, authority: &Pubkey, amount: u64, decimals: u8) -> Instruction { let mut data = Vec::with_capacity(10); data.push(12); data.extend_from_slice(&amount.to_le_bytes()); data.push(decimals); Instruction { program_id: *token, accounts: vec![ AccountMeta::new(*from, false), AccountMeta::new_readonly(*mint, false), AccountMeta::new(*to, false), AccountMeta::new_readonly(*authority, true), ], data, } } fn mint_decimals(mint: &AccountInfo) -> Result { let d = mint.try_borrow_data()?; d.get(44).copied().ok_or_else(|| VaultError::WrongAccount.into()) } fn spent_address(program_id: &Pubkey, vault: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address(&[b"spent", vault.as_ref()], program_id) } fn spent_marker(program_id: &Pubkey, vault: &Pubkey, marker: &AccountInfo) -> Result { let (expect, _) = spent_address(program_id, vault); key_is(marker, &expect)?; if marker.owner == program_id { let data = marker.try_borrow_data()?; if data.len() != SPENT_LEN || data[0] != SPENT_TAG { return Err(VaultError::WrongStage.into()); } return Ok(true); } if marker.owner != &system_program::ID || !marker.data_is_empty() { return Err(VaultError::WrongAccount.into()); } Ok(false) } fn ensure_unspent(program_id: &Pubkey, vault: &Pubkey, marker: &AccountInfo) -> ProgramResult { if spent_marker(program_id, vault, marker)? { return Err(VaultError::AlreadySpent.into()); } Ok(()) } fn record_spent<'a>(program_id: &Pubkey, payer: &AccountInfo<'a>, vault: &Pubkey, marker: &AccountInfo<'a>, system: &AccountInfo<'a>) -> ProgramResult { let (expect, bump) = spent_address(program_id, vault); key_is(marker, &expect)?; if spent_marker(program_id, vault, marker)? { return Err(VaultError::AlreadySpent.into()); } key_is(system, &system_program::ID)?; let seeds: &[&[u8]] = &[b"spent", vault.as_ref(), &[bump]]; let rent = Rent::get()?.minimum_balance(SPENT_LEN); let have = marker.lamports(); if have == 0 { invoke_signed( &system_instruction::create_account(payer.key, marker.key, rent, SPENT_LEN as u64, program_id), &[payer.clone(), marker.clone(), system.clone()], &[seeds], )?; } else { if have < rent { invoke( &system_instruction::transfer(payer.key, marker.key, rent - have), &[payer.clone(), marker.clone(), system.clone()], )?; } invoke_signed(&system_instruction::allocate(marker.key, SPENT_LEN as u64), &[marker.clone(), system.clone()], &[seeds])?; invoke_signed(&system_instruction::assign(marker.key, program_id), &[marker.clone(), system.clone()], &[seeds])?; } marker.try_borrow_mut_data()?[0] = SPENT_TAG; Ok(()) } fn token_amount(acc: &AccountInfo) -> Result { let d = acc.try_borrow_data()?; d.get(64..72).and_then(|s| s.try_into().ok()).map(u64::from_le_bytes).ok_or_else(|| VaultError::WrongAccount.into()) } // ───────────────────────── entry ───────────────────────── pub fn process_instruction(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { match data.first() { Some(0) => stage(program_id, accounts, data), Some(1) => execute(program_id, accounts, data, false), Some(2) => execute(program_id, accounts, data, true), Some(3) => deposit_sol(program_id, accounts, data), Some(4) => deposit_token(program_id, accounts, data), Some(5) => cancel(program_id, accounts), _ => Err(VaultError::BadInstruction.into()), } } fn stage(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { let it = &mut accounts.iter(); let payer = next_account_info(it)?; let stage = next_account_info(it)?; let vault = next_account_info(it)?; let system = next_account_info(it)?; signer(payer)?; key_is(system, &system_program::ID)?; // tag 1, vault_bump 1, stage_bump 1, mint 32, recipient 32, refund 32, amount 8, half sig 420 if data.len() != 3 + 96 + 8 + HALF { return Err(VaultError::BadInstruction.into()); } let vbump = data[1]; let sbump = data[2]; let seeds: &[&[u8]] = &[b"stage", vault.key.as_ref(), payer.key.as_ref(), &[sbump]]; let expect = Pubkey::create_program_address(seeds, program_id).map_err(|_| VaultError::WrongStage)?; key_is(stage, &expect)?; if read32(data, 3 + 64)? == vault.key.to_bytes() { return Err(VaultError::SameVault.into()); } if stage.owner != program_id { let rent = Rent::get()?.minimum_balance(STAGE_LEN); let have = stage.lamports(); if have == 0 { invoke_signed(&system_instruction::create_account(payer.key, stage.key, rent, STAGE_LEN as u64, program_id), &[payer.clone(), stage.clone(), system.clone()], &[seeds])?; } else { if have < rent { invoke(&system_instruction::transfer(payer.key, stage.key, rent - have), &[payer.clone(), stage.clone(), system.clone()])?; } invoke_signed(&system_instruction::allocate(stage.key, STAGE_LEN as u64), &[stage.clone(), system.clone()], &[seeds])?; invoke_signed(&system_instruction::assign(stage.key, program_id), &[stage.clone(), system.clone()], &[seeds])?; } } let mut d = stage.try_borrow_mut_data()?; if d.len() != STAGE_LEN { return Err(VaultError::WrongStage.into()); } d[ST_TAG] = STAGE_TAG; d[ST_VAULT..ST_VAULT + 32].copy_from_slice(vault.key.as_ref()); d[ST_PAYER..ST_PAYER + 32].copy_from_slice(payer.key.as_ref()); d[ST_VBUMP] = vbump; d[ST_MINT..ST_SIG].copy_from_slice(&data[3..3 + 104]); d[ST_SIG..ST_SIG + HALF].copy_from_slice(&data[3 + 104..]); msg!("tardigrade: staged"); Ok(()) } struct Staged { vault: Pubkey, payer: Pubkey, vbump: u8, mint: [u8; 32], recipient: [u8; 32], refund: [u8; 32], amount: u64, sig: [u8; SIG_LEN], } fn load_stage(program_id: &Pubkey, stage: &AccountInfo, tail: &[u8]) -> Result { if stage.owner != program_id { return Err(VaultError::WrongStage.into()); } let d = stage.try_borrow_data()?; if d.len() != STAGE_LEN || d[ST_TAG] != STAGE_TAG || tail.len() != HALF { return Err(VaultError::WrongStage.into()); } let mut sig = [0u8; SIG_LEN]; sig[..HALF].copy_from_slice(&d[ST_SIG..ST_SIG + HALF]); sig[HALF..].copy_from_slice(tail); Ok(Staged { vault: Pubkey::new_from_array(read32(&d, ST_VAULT)?), payer: Pubkey::new_from_array(read32(&d, ST_PAYER)?), vbump: d[ST_VBUMP], mint: read32(&d, ST_MINT)?, recipient: read32(&d, ST_RECIP)?, refund: read32(&d, ST_REFUND)?, amount: read_u64(&d, ST_AMOUNT)?, sig, }) } fn close_stage(stage: &AccountInfo, to: &AccountInfo) -> ProgramResult { let l = stage.lamports(); **to.try_borrow_mut_lamports()? += l; **stage.try_borrow_mut_lamports()? = 0; stage.realloc(0, false)?; stage.assign(&system_program::ID); Ok(()) } fn execute(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8], token: bool) -> ProgramResult { let it = &mut accounts.iter(); let payer = next_account_info(it)?; let stage = next_account_info(it)?; let vault = next_account_info(it)?; signer(payer)?; let st = load_stage(program_id, stage, &data[1..])?; key_is(vault, &st.vault)?; key_is(payer, &st.payer)?; if (st.mint == [0u8; 32]) == token { return Err(VaultError::BadInstruction.into()); } // the part that matters: hash chains back to the vault's own seed let dg = digest(program_id, &st.vault, &st.mint, &st.recipient, &st.refund, st.amount); let root = recover_root(&st.sig, &dg); let vseeds: &[&[u8]] = &[b"vault", &root, &[st.vbump]]; let expect = Pubkey::create_program_address(vseeds, program_id).map_err(|_| VaultError::BadSignature)?; if expect != st.vault { msg!("tardigrade: signature does not open this vault"); return Err(VaultError::BadSignature.into()); } if !token { let recipient = next_account_info(it)?; let refund = next_account_info(it)?; let system = next_account_info(it)?; let spent = next_account_info(it)?; key_is(recipient, &Pubkey::new_from_array(st.recipient))?; key_is(refund, &Pubkey::new_from_array(st.refund))?; key_is(system, &system_program::ID)?; record_spent(program_id, payer, vault.key, spent, system)?; let total = vault.lamports(); if st.amount > total { return Err(VaultError::NotEnough.into()); } if st.amount > 0 { invoke_signed(&system_instruction::transfer(vault.key, recipient.key, st.amount), &[vault.clone(), recipient.clone(), system.clone()], &[vseeds])?; } let rest = total - st.amount; if rest > 0 { invoke_signed(&system_instruction::transfer(vault.key, refund.key, rest), &[vault.clone(), refund.clone(), system.clone()], &[vseeds])?; } msg!("tardigrade: sol {} out, {} rolled", st.amount, rest); } else { let vault_ata = next_account_info(it)?; let recipient = next_account_info(it)?; let recipient_ata = next_account_info(it)?; let refund = next_account_info(it)?; let refund_ata = next_account_info(it)?; let mint = next_account_info(it)?; let tprog = next_account_info(it)?; let ata_prog = next_account_info(it)?; let system = next_account_info(it)?; let spent = next_account_info(it)?; key_is(recipient, &Pubkey::new_from_array(st.recipient))?; key_is(refund, &Pubkey::new_from_array(st.refund))?; key_is(mint, &Pubkey::new_from_array(st.mint))?; key_is(ata_prog, &ATA_PROGRAM)?; key_is(system, &system_program::ID)?; token_program_ok(tprog)?; if mint.owner != tprog.key || vault_ata.owner != tprog.key { return Err(VaultError::WrongTokenProgram.into()); } // the vault's token account must be the canonical one let (vexp, _) = Pubkey::find_program_address(&[vault.key.as_ref(), tprog.key.as_ref(), mint.key.as_ref()], &ATA_PROGRAM); key_is(vault_ata, &vexp)?; let decimals = mint_decimals(mint)?; let total = token_amount(vault_ata)?; if st.amount > total { return Err(VaultError::NotEnough.into()); } record_spent(program_id, payer, vault.key, spent, system)?; if st.amount > 0 { ata_create_idempotent(payer, recipient_ata, recipient, mint, system, tprog, ata_prog)?; invoke_signed(&transfer_checked_ix(tprog.key, vault_ata.key, mint.key, recipient_ata.key, vault.key, st.amount, decimals), &[vault_ata.clone(), mint.clone(), recipient_ata.clone(), vault.clone(), tprog.clone()], &[vseeds])?; } let rest = total - st.amount; if rest > 0 { ata_create_idempotent(payer, refund_ata, refund, mint, system, tprog, ata_prog)?; invoke_signed(&transfer_checked_ix(tprog.key, vault_ata.key, mint.key, refund_ata.key, vault.key, rest, decimals), &[vault_ata.clone(), mint.clone(), refund_ata.clone(), vault.clone(), tprog.clone()], &[vseeds])?; } // hand the empty token account's rent back to whoever paid for this spend if *tprog.key == TOKEN_PROGRAM { let ix = Instruction { program_id: TOKEN_PROGRAM, accounts: vec![AccountMeta::new(*vault_ata.key, false), AccountMeta::new(*payer.key, false), AccountMeta::new_readonly(*vault.key, true)], data: vec![9], }; invoke_signed(&ix, &[vault_ata.clone(), payer.clone(), vault.clone(), tprog.clone()], &[vseeds])?; } msg!("tardigrade: token {} out, {} rolled", st.amount, rest); } close_stage(stage, payer)?; Ok(()) } fn cancel(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let it = &mut accounts.iter(); let payer = next_account_info(it)?; let stage = next_account_info(it)?; signer(payer)?; if stage.owner != program_id { return Err(VaultError::WrongStage.into()); } { let d = stage.try_borrow_data()?; if d.len() != STAGE_LEN || d[ST_TAG] != STAGE_TAG || &d[ST_PAYER..ST_PAYER + 32] != payer.key.as_ref() { return Err(VaultError::WrongStage.into()); } } close_stage(stage, payer) } fn deposit_sol(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { let it = &mut accounts.iter(); let payer = next_account_info(it)?; let vault = next_account_info(it)?; let system = next_account_info(it)?; let spent = next_account_info(it)?; signer(payer)?; key_is(system, &system_program::ID)?; ensure_unspent(program_id, vault.key, spent)?; let amount = read_u64(data, 1)?; invoke(&system_instruction::transfer(payer.key, vault.key, amount), &[payer.clone(), vault.clone(), system.clone()])?; msg!("tardigrade: deposit sol {}", amount); Ok(()) } fn deposit_token(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { let it = &mut accounts.iter(); let payer = next_account_info(it)?; let payer_ata = next_account_info(it)?; let vault = next_account_info(it)?; let vault_ata = next_account_info(it)?; let mint = next_account_info(it)?; let tprog = next_account_info(it)?; let ata_prog = next_account_info(it)?; let system = next_account_info(it)?; let spent = next_account_info(it)?; signer(payer)?; token_program_ok(tprog)?; key_is(ata_prog, &ATA_PROGRAM)?; key_is(system, &system_program::ID)?; ensure_unspent(program_id, vault.key, spent)?; let amount = read_u64(data, 1)?; let decimals = mint_decimals(mint)?; ata_create_idempotent(payer, vault_ata, vault, mint, system, tprog, ata_prog)?; invoke(&transfer_checked_ix(tprog.key, payer_ata.key, mint.key, vault_ata.key, payer.key, amount, decimals), &[payer_ata.clone(), mint.clone(), vault_ata.clone(), payer.clone(), tprog.clone()])?; msg!("tardigrade: deposit token {}", amount); Ok(()) } #[cfg(test)] mod tests { use super::*; fn secrets(seed: u8) -> [[u8; HASH_LEN]; CHAINS] { let mut s = [[0u8; HASH_LEN]; CHAINS]; for i in 0..CHAINS { let h = hashv(&[&[seed, i as u8]]).to_bytes(); s[i].copy_from_slice(&h[..HASH_LEN]); } s } #[test] fn roundtrip() { let sk = secrets(7); let root = public_root(&sk); let pid = Pubkey::new_from_array([3u8; 32]); let vault = Pubkey::new_from_array([9u8; 32]); let dg = digest(&pid, &vault, &[0u8; 32], &[1u8; 32], &[2u8; 32], 1_000_000); let sig = sign(&sk, &dg); assert_eq!(recover_root(&sig, &dg), root); // a different amount must not verify let dg2 = digest(&pid, &vault, &[0u8; 32], &[1u8; 32], &[2u8; 32], 1_000_001); assert_ne!(recover_root(&sig, &dg2), root); // a flipped byte must not verify let mut bad = sig; bad[400] ^= 1; assert_ne!(recover_root(&bad, &dg), root); } #[test] fn checksum_covers() { let mut d = [0u8; HASH_LEN]; let a = digits(&d); d[0] = 1; let b = digits(&d); // raising a message digit lowers the checksum, so no digit vector dominates another assert!(b[0] > a[0] && (((b[28] as u32) << 8) | b[29] as u32) < (((a[28] as u32) << 8) | a[29] as u32)); } /// prints a vector the browser implementation is checked against #[test] fn vector() { let sk = secrets(1); let root = public_root(&sk); let pid = Pubkey::new_from_array([3u8; 32]); let vault = Pubkey::new_from_array([9u8; 32]); let dg = digest(&pid, &vault, &[5u8; 32], &[1u8; 32], &[2u8; 32], 42); let sig = sign(&sk, &dg); let hex = |b: &[u8]| b.iter().map(|x| format!("{:02x}", x)).collect::(); println!("VECTOR {{\"sk0\":\"{}\",\"root\":\"{}\",\"digest\":\"{}\",\"sig\":\"{}\"}}", hex(&sk[0]), hex(&root), hex(&dg), hex(&sig)); } }