@ -47,6 +47,7 @@ mod extended_key;
mod utils ;
mod address ;
mod prover ;
pub mod bugs ;
use data ::{ BlockData , WalletTx , Utxo , SaplingNoteData , SpendableNote , OutgoingTxMetadata } ;
use extended_key ::{ KeyIndex , ExtendedPrivKey } ;
@ -90,17 +91,30 @@ impl ToBase58Check for [u8] {
}
pub struct LightWallet {
seed : [ u8 ; 32 ] , // Seed phrase for this wallet.
// Is the wallet encrypted? If it is, then when writing to disk, the seed is always encrypted
// and the individual spending keys are not written
encrypted : bool ,
// List of keys, actually in this wallet. This may include more
// than keys derived from the seed, for example, if user imports
// a private key
// In memory only (i.e, this field is not written to disk). Is the wallet unlocked and are
// the spending keys present to allow spending from this wallet?
unlocked : bool ,
enc_seed : [ u8 ; 48 ] , // If locked, this contains the encrypted seed
nonce : Vec < u8 > , // Nonce used to encrypt the wallet.
seed : [ u8 ; 32 ] , // Seed phrase for this wallet. If wallet is locked, this is 0
// List of keys, actually in this wallet. If the wallet is locked, the `extsks` will be
// encrypted (but the fvks are not encrpyted)
extsks : Arc < RwLock < Vec < ExtendedSpendingKey > > > ,
extfvks : Arc < RwLock < Vec < ExtendedFullViewingKey > > > ,
pub address : Arc < RwLock < Vec < PaymentAddress < Bls12 > > > > ,
pub zaddress : Arc < RwLock < Vec < PaymentAddress < Bls12 > > > > ,
// Transparent keys. TODO: Make it not pubic
pub tkeys : Arc < RwLock < Vec < secp256k1 ::SecretKey > > > ,
// Transparent keys. If the wallet is locked, then the secret keys will be encrypted,
// but the addresses will be present.
tkeys : Arc < RwLock < Vec < secp256k1 ::SecretKey > > > ,
pub taddresses : Arc < RwLock < Vec < String > > > ,
blocks : Arc < RwLock < Vec < BlockData > > > ,
pub txs : Arc < RwLock < HashMap < TxId , WalletTx > > > ,
@ -115,10 +129,12 @@ pub struct LightWallet {
impl LightWallet {
pub fn serialized_version ( ) -> u64 {
return 3 ;
return 4 ;
}
fn get_taddr_from_bip39seed ( config : & LightClientConfig , bip39_seed : & [ u8 ] , pos : u32 ) -> SecretKey {
assert_eq ! ( bip39_seed . len ( ) , 64 ) ;
let ext_t_key = ExtendedPrivKey ::with_seed ( bip39_seed ) . unwrap ( ) ;
ext_t_key
. derive_private_key ( KeyIndex ::hardened_from_normalize_index ( 44 ) . unwrap ( ) ) . unwrap ( )
@ -130,10 +146,12 @@ impl LightWallet {
}
fn get_zaddr_from_bip39seed ( config : & LightClientConfig , bip39seed : & [ u8 ] , pos : u32 ) ->
fn get_zaddr_from_bip39seed ( config : & LightClientConfig , bip39_ seed : & [ u8 ] , pos : u32 ) ->
( ExtendedSpendingKey , ExtendedFullViewingKey , PaymentAddress < Bls12 > ) {
assert_eq ! ( bip39_seed . len ( ) , 64 ) ;
let extsk : ExtendedSpendingKey = ExtendedSpendingKey ::from_path (
& ExtendedSpendingKey ::master ( bip39seed ) ,
& ExtendedSpendingKey ::master ( bip39_ seed ) ,
& [
ChildIndex ::Hardened ( 32 ) ,
ChildIndex ::Hardened ( config . get_coin_type ( ) ) ,
@ -163,8 +181,9 @@ impl LightWallet {
// we need to get the 64 byte bip39 entropy
let bip39_seed = bip39 ::Seed ::new ( & Mnemonic ::from_entropy ( & seed_bytes , Language ::English ) . unwrap ( ) , "" ) ;
// Derive only the first address
// Derive only the first sk and address
let tpk = LightWallet ::get_taddr_from_bip39seed ( & config , & bip39_seed . as_bytes ( ) , 0 ) ;
let taddr = LightWallet ::address_from_prefix_sk ( & config . base58_pubkey_address ( ) , & tpk ) ;
// TODO: We need to monitor addresses, and always keep 1 "free" address, so
// users can import a seed phrase and automatically get all used addresses
@ -172,23 +191,50 @@ impl LightWallet {
= LightWallet ::get_zaddr_from_bip39seed ( & config , & bip39_seed . as_bytes ( ) , 0 ) ;
Ok ( LightWallet {
seed : seed_bytes ,
extsks : Arc ::new ( RwLock ::new ( vec ! [ extsk ] ) ) ,
extfvks : Arc ::new ( RwLock ::new ( vec ! [ extfvk ] ) ) ,
address : Arc ::new ( RwLock ::new ( vec ! [ address ] ) ) ,
tkeys : Arc ::new ( RwLock ::new ( vec ! [ tpk ] ) ) ,
blocks : Arc ::new ( RwLock ::new ( vec ! [ ] ) ) ,
txs : Arc ::new ( RwLock ::new ( HashMap ::new ( ) ) ) ,
config : config . clone ( ) ,
birthday : latest_block ,
encrypted : false ,
unlocked : true ,
enc_seed : [ 0 u8 ; 48 ] ,
nonce : vec ! [ ] ,
seed : seed_bytes ,
extsks : Arc ::new ( RwLock ::new ( vec ! [ extsk ] ) ) ,
extfvks : Arc ::new ( RwLock ::new ( vec ! [ extfvk ] ) ) ,
zaddress : Arc ::new ( RwLock ::new ( vec ! [ address ] ) ) ,
tkeys : Arc ::new ( RwLock ::new ( vec ! [ tpk ] ) ) ,
taddresses : Arc ::new ( RwLock ::new ( vec ! [ taddr ] ) ) ,
blocks : Arc ::new ( RwLock ::new ( vec ! [ ] ) ) ,
txs : Arc ::new ( RwLock ::new ( HashMap ::new ( ) ) ) ,
config : config . clone ( ) ,
birthday : latest_block ,
} )
}
pub fn read < R : Read > ( mut reader : R , config : & LightClientConfig ) -> io ::Result < Self > {
let version = reader . read_u64 ::< LittleEndian > ( ) ? ;
assert ! ( version < = LightWallet ::serialized_version ( ) ) ;
if version > LightWallet ::serialized_version ( ) {
let e = format ! ( "Don't know how to read wallet version {}. Do you have the latest version?" , version ) ;
error ! ( "{}" , e ) ;
return Err ( io ::Error ::new ( ErrorKind ::InvalidData , e ) ) ;
}
info ! ( "Reading wallet version {}" , version ) ;
let encrypted = if version > = 4 {
reader . read_u8 ( ) ? > 0
} else {
false
} ;
let mut enc_seed = [ 0 u8 ; 48 ] ;
if version > = 4 {
reader . read_exact ( & mut enc_seed ) ? ;
}
let nonce = if version > = 4 {
Vector ::read ( & mut reader , | r | r . read_u8 ( ) ) ?
} else {
vec ! [ ]
} ;
// Seed
let mut seed_bytes = [ 0 u8 ; 32 ] ;
reader . read_exact ( & mut seed_bytes ) ? ;
@ -196,9 +242,14 @@ impl LightWallet {
// Read the spending keys
let extsks = Vector ::read ( & mut reader , | r | ExtendedSpendingKey ::read ( r ) ) ? ;
// Calculate the viewing keys
let extfvks = extsks . iter ( ) . map ( | sk | ExtendedFullViewingKey ::from ( sk ) )
. collect ::< Vec < ExtendedFullViewingKey > > ( ) ;
let extfvks = if version > = 4 {
// Read the viewing keys
Vector ::read ( & mut reader , | r | ExtendedFullViewingKey ::read ( r ) ) ?
} else {
// Calculate the viewing keys
extsks . iter ( ) . map ( | sk | ExtendedFullViewingKey ::from ( sk ) )
. collect ::< Vec < ExtendedFullViewingKey > > ( )
} ;
// Calculate the addresses
let addresses = extfvks . iter ( ) . map ( | fvk | fvk . default_address ( ) . unwrap ( ) . 1 )
@ -210,6 +261,14 @@ impl LightWallet {
secp256k1 ::SecretKey ::from_slice ( & tpk_bytes ) . map_err ( | e | io ::Error ::new ( ErrorKind ::InvalidData , e ) )
} ) ? ;
let taddresses = if version > = 4 {
// Read the addresses
Vector ::read ( & mut reader , | r | utils ::read_string ( r ) ) ?
} else {
// Calculate the addresses
tkeys . iter ( ) . map ( | sk | LightWallet ::address_from_prefix_sk ( & config . base58_pubkey_address ( ) , sk ) ) . collect ( )
} ;
let blocks = Vector ::read ( & mut reader , | r | BlockData ::read ( r ) ) ? ;
let txs_tuples = Vector ::read ( & mut reader , | r | {
@ -230,22 +289,41 @@ impl LightWallet {
let birthday = reader . read_u64 ::< LittleEndian > ( ) ? ;
Ok ( LightWallet {
seed : seed_bytes ,
extsks : Arc ::new ( RwLock ::new ( extsks ) ) ,
extfvks : Arc ::new ( RwLock ::new ( extfvks ) ) ,
address : Arc ::new ( RwLock ::new ( addresses ) ) ,
tkeys : Arc ::new ( RwLock ::new ( tkeys ) ) ,
blocks : Arc ::new ( RwLock ::new ( blocks ) ) ,
txs : Arc ::new ( RwLock ::new ( txs ) ) ,
config : config . clone ( ) ,
encrypted : encrypted ,
unlocked : ! encrypted , // When reading from disk, if wallet is encrypted, it starts off locked.
enc_seed : enc_seed ,
nonce : nonce ,
seed : seed_bytes ,
extsks : Arc ::new ( RwLock ::new ( extsks ) ) ,
extfvks : Arc ::new ( RwLock ::new ( extfvks ) ) ,
zaddress : Arc ::new ( RwLock ::new ( addresses ) ) ,
tkeys : Arc ::new ( RwLock ::new ( tkeys ) ) ,
taddresses : Arc ::new ( RwLock ::new ( taddresses ) ) ,
blocks : Arc ::new ( RwLock ::new ( blocks ) ) ,
txs : Arc ::new ( RwLock ::new ( txs ) ) ,
config : config . clone ( ) ,
birthday ,
} )
}
pub fn write < W : Write > ( & self , mut writer : W ) -> io ::Result < ( ) > {
if self . encrypted & & self . unlocked {
return Err ( Error ::new ( ErrorKind ::InvalidInput ,
format ! ( "Cannot write while wallet is unlocked while encrypted." ) ) ) ;
}
// Write the version
writer . write_u64 ::< LittleEndian > ( LightWallet ::serialized_version ( ) ) ? ;
// Write if it is locked
writer . write_u8 ( if self . encrypted { 1 } else { 0 } ) ? ;
// Write the encrypted seed bytes
writer . write_all ( & self . enc_seed ) ? ;
// Write the nonce
Vector ::write ( & mut writer , & self . nonce , | w , b | w . write_u8 ( * b ) ) ? ;
// Write the seed
writer . write_all ( & self . seed ) ? ;
@ -257,11 +335,21 @@ impl LightWallet {
| w , sk | sk . write ( w )
) ? ;
// Write the transparent private key
// Write the FVKs
Vector ::write ( & mut writer , & self . extfvks . read ( ) . unwrap ( ) ,
| w , fvk | fvk . write ( w )
) ? ;
// Write the transparent private keys
Vector ::write ( & mut writer , & self . tkeys . read ( ) . unwrap ( ) ,
| w , pk | w . write_all ( & pk [ . . ] )
) ? ;
// Write the transparent addresses
Vector ::write ( & mut writer , & self . taddresses . read ( ) . unwrap ( ) ,
| w , a | utils ::write_string ( w , a )
) ? ;
Vector ::write ( & mut writer , & self . blocks . read ( ) . unwrap ( ) , | w , b | b . write ( w ) ) ? ;
// The hashmap, write as a set of tuples
@ -279,9 +367,9 @@ impl LightWallet {
Ok ( ( ) )
}
pub fn note_address ( & self , note : & SaplingNoteData ) -> Option < String > {
pub fn note_address ( hrp : & str , note : & SaplingNoteData ) -> Option < String > {
match note . extfvk . fvk . vk . into_payment_address ( note . diversifier , & JUBJUB ) {
Some ( pa ) = > Some ( encode_payment_address ( self . config . hrp_sapling_address ( ) , & pa ) ) ,
Some ( pa ) = > Some ( encode_payment_address ( hrp , & pa ) ) ,
None = > None
}
}
@ -317,7 +405,8 @@ impl LightWallet {
/// Get all t-address private keys. Returns a Vector of (address, secretkey)
pub fn get_t_secret_keys ( & self ) -> Vec < ( String , String ) > {
self . tkeys . read ( ) . unwrap ( ) . iter ( ) . map ( | sk | {
( self . address_from_sk ( sk ) , sk [ . . ] . to_base58check ( & self . config . base58_secretkey_prefix ( ) , & [ 0x01 ] ) )
( self . address_from_sk ( sk ) ,
sk [ . . ] . to_base58check ( & self . config . base58_secretkey_prefix ( ) , & [ 0x01 ] ) )
} ) . collect ::< Vec < ( String , String ) > > ( )
}
@ -325,14 +414,20 @@ impl LightWallet {
/// at the next position and add it to the wallet.
/// NOTE: This does NOT rescan
pub fn add_zaddr ( & self ) -> String {
if ! self . unlocked {
return "" . to_string ( ) ;
}
let pos = self . extsks . read ( ) . unwrap ( ) . len ( ) as u32 ;
let bip39_seed = bip39 ::Seed ::new ( & Mnemonic ::from_entropy ( & self . seed , Language ::English ) . unwrap ( ) , "" ) ;
let ( extsk , extfvk , address ) =
LightWallet ::get_zaddr_from_bip39seed ( & self . config , & self . seed , pos ) ;
LightWallet ::get_zaddr_from_bip39seed ( & self . config , & bip39_seed . as_bytes ( ) , pos ) ;
let zaddr = encode_payment_address ( self . config . hrp_sapling_address ( ) , & address ) ;
self . extsks . write ( ) . unwrap ( ) . push ( extsk ) ;
self . extfvks . write ( ) . unwrap ( ) . push ( extfvk ) ;
self . address . write ( ) . unwrap ( ) . push ( address ) ;
self . z address. write ( ) . unwrap ( ) . push ( address ) ;
zaddr
}
@ -341,12 +436,20 @@ impl LightWallet {
/// at the next position.
/// NOTE: This is not rescan the wallet
pub fn add_taddr ( & self ) -> String {
if ! self . unlocked {
return "" . to_string ( ) ;
}
let pos = self . tkeys . read ( ) . unwrap ( ) . len ( ) as u32 ;
let sk = LightWallet ::get_taddr_from_bip39seed ( & self . config , & self . seed , pos ) ;
let bip39_seed = bip39 ::Seed ::new ( & Mnemonic ::from_entropy ( & self . seed , Language ::English ) . unwrap ( ) , "" ) ;
let sk = LightWallet ::get_taddr_from_bip39seed ( & self . config , & bip39_seed . as_bytes ( ) , pos ) ;
let address = self . address_from_sk ( & sk ) ;
self . tkeys . write ( ) . unwrap ( ) . push ( sk ) ;
self . taddresses . write ( ) . unwrap ( ) . push ( address . clone ( ) ) ;
self . address_from_sk ( & sk )
address
}
/// Clears all the downloaded blocks and resets the state back to the initial block.
@ -453,7 +556,7 @@ impl LightWallet {
}
}
pub fn address_from_sk ( & self , sk : & secp256k1 ::SecretKey ) -> String {
pub fn address_from_prefix_sk ( prefix : & [ u8 ; 1 ] , sk : & secp256k1 ::SecretKey ) -> String {
let secp = secp256k1 ::Secp256k1 ::new ( ) ;
let pk = secp256k1 ::PublicKey ::from_secret_key ( & secp , & sk ) ;
@ -461,7 +564,11 @@ impl LightWallet {
let mut hash160 = ripemd160 ::Ripemd160 ::new ( ) ;
hash160 . input ( Sha256 ::digest ( & pk . serialize ( ) [ . . ] . to_vec ( ) ) ) ;
hash160 . result ( ) . to_base58check ( & self . config . base58_pubkey_address ( ) , & [ ] )
hash160 . result ( ) . to_base58check ( prefix , & [ ] )
}
pub fn address_from_sk ( & self , sk : & secp256k1 ::SecretKey ) -> String {
LightWallet ::address_from_prefix_sk ( & self . config . base58_pubkey_address ( ) , sk )
}
pub fn address_from_pubkeyhash ( & self , ta : Option < TransparentAddress > ) -> Option < String > {
@ -477,11 +584,149 @@ impl LightWallet {
}
pub fn get_seed_phrase ( & self ) -> String {
if ! self . unlocked {
return "" . to_string ( ) ;
}
Mnemonic ::from_entropy ( & self . seed ,
Language ::English ,
) . unwrap ( ) . phrase ( ) . to_string ( )
}
pub fn encrypt ( & mut self , passwd : String ) -> io ::Result < ( ) > {
use sodiumoxide ::crypto ::secretbox ;
if self . encrypted & & ! self . unlocked {
return Err ( io ::Error ::new ( ErrorKind ::AlreadyExists , "Wallet is already encrypted and locked" ) ) ;
}
// Get the doublesha256 of the password, which is the right length
let key = secretbox ::Key ::from_slice ( & double_sha256 ( passwd . as_bytes ( ) ) ) . unwrap ( ) ;
let nonce = secretbox ::gen_nonce ( ) ;
let cipher = secretbox ::seal ( & self . seed , & nonce , & key ) ;
self . enc_seed . copy_from_slice ( & cipher ) ;
self . nonce = vec ! [ ] ;
self . nonce . extend_from_slice ( nonce . as_ref ( ) ) ;
self . encrypted = true ;
self . lock ( ) ? ;
Ok ( ( ) )
}
pub fn lock ( & mut self ) -> io ::Result < ( ) > {
// Empty the seed and the secret keys
self . seed . copy_from_slice ( & [ 0 u8 ; 32 ] ) ;
self . tkeys = Arc ::new ( RwLock ::new ( vec ! [ ] ) ) ;
self . extsks = Arc ::new ( RwLock ::new ( vec ! [ ] ) ) ;
self . unlocked = false ;
Ok ( ( ) )
}
pub fn unlock ( & mut self , passwd : String ) -> io ::Result < ( ) > {
use sodiumoxide ::crypto ::secretbox ;
if ! self . encrypted {
return Err ( Error ::new ( ErrorKind ::AlreadyExists , "Wallet is not encrypted" ) ) ;
}
if self . encrypted & & self . unlocked {
return Err ( Error ::new ( ErrorKind ::AlreadyExists , "Wallet is already unlocked" ) ) ;
}
// Get the doublesha256 of the password, which is the right length
let key = secretbox ::Key ::from_slice ( & double_sha256 ( passwd . as_bytes ( ) ) ) . unwrap ( ) ;
let nonce = secretbox ::Nonce ::from_slice ( & self . nonce ) . unwrap ( ) ;
let seed = match secretbox ::open ( & self . enc_seed , & nonce , & key ) {
Ok ( s ) = > s ,
Err ( _ ) = > { return Err ( io ::Error ::new ( ErrorKind ::InvalidData , "Decryption failed. Is your password correct?" ) ) ; }
} ;
// Now that we have the seed, we'll generate the extsks and tkeys, and verify the fvks and addresses
// respectively match
// The seed bytes is the raw entropy. To pass it to HD wallet generation,
// we need to get the 64 byte bip39 entropy
let bip39_seed = bip39 ::Seed ::new ( & Mnemonic ::from_entropy ( & seed , Language ::English ) . unwrap ( ) , "" ) ;
// Sapling keys
let mut extsks = vec ! [ ] ;
for pos in 0 . . self . zaddress . read ( ) . unwrap ( ) . len ( ) {
let ( extsk , extfvk , address ) =
LightWallet ::get_zaddr_from_bip39seed ( & self . config , & bip39_seed . as_bytes ( ) , pos as u32 ) ;
if address ! = self . zaddress . read ( ) . unwrap ( ) [ pos ] {
return Err ( io ::Error ::new ( ErrorKind ::InvalidData ,
format ! ( "zaddress mismatch at {}. {:?} vs {:?}" , pos , address , self . zaddress . read ( ) . unwrap ( ) [ pos ] ) ) ) ;
}
if extfvk ! = self . extfvks . read ( ) . unwrap ( ) [ pos ] {
return Err ( io ::Error ::new ( ErrorKind ::InvalidData ,
format ! ( "fvk mismatch at {}. {:?} vs {:?}" , pos , extfvk , self . extfvks . read ( ) . unwrap ( ) [ pos ] ) ) ) ;
}
// Don't add it to self yet, we'll do that at the end when everything is verified
extsks . push ( extsk ) ;
}
// Transparent keys
let mut tkeys = vec ! [ ] ;
for pos in 0 . . self . taddresses . read ( ) . unwrap ( ) . len ( ) {
let sk = LightWallet ::get_taddr_from_bip39seed ( & self . config , & bip39_seed . as_bytes ( ) , pos as u32 ) ;
let address = self . address_from_sk ( & sk ) ;
if address ! = self . taddresses . read ( ) . unwrap ( ) [ pos ] {
return Err ( io ::Error ::new ( ErrorKind ::InvalidData ,
format ! ( "taddress mismatch at {}. {} vs {}" , pos , address , self . taddresses . read ( ) . unwrap ( ) [ pos ] ) ) ) ;
}
tkeys . push ( sk ) ;
}
// Everything checks out, so we'll update our wallet with the decrypted values
self . extsks = Arc ::new ( RwLock ::new ( extsks ) ) ;
self . tkeys = Arc ::new ( RwLock ::new ( tkeys ) ) ;
self . seed . copy_from_slice ( & seed ) ;
self . encrypted = true ;
self . unlocked = true ;
Ok ( ( ) )
}
// Removing encryption means unlocking it and setting the self.encrypted = false,
// permanantly removing the encryption
pub fn remove_encryption ( & mut self , passwd : String ) -> io ::Result < ( ) > {
if ! self . encrypted {
return Err ( Error ::new ( ErrorKind ::AlreadyExists , "Wallet is not encrypted" ) ) ;
}
// Unlock the wallet if it's locked
if ! self . unlocked {
self . unlock ( passwd ) ? ;
}
// Permanantly remove the encryption
self . encrypted = false ;
self . nonce = vec ! [ ] ;
self . enc_seed . copy_from_slice ( & [ 0 u8 ; 48 ] ) ;
Ok ( ( ) )
}
pub fn is_encrypted ( & self ) -> bool {
return self . encrypted ;
}
pub fn is_unlocked_for_spending ( & self ) -> bool {
return self . unlocked ;
}
pub fn zbalance ( & self , addr : Option < String > ) -> u64 {
self . txs . read ( ) . unwrap ( )
. values ( )
@ -560,12 +805,12 @@ impl LightWallet {
. sum ::< u64 > ( )
}
fn add_toutput_to_wtx ( & self , height : i32 , txid : & TxId , vout : & TxOut , n : u64 ) {
fn add_toutput_to_wtx ( & self , height : i32 , timestamp : u64 , t xid : & TxId , vout : & TxOut , n : u64 ) {
let mut txs = self . txs . write ( ) . unwrap ( ) ;
// Find the existing transaction entry, or create a new one.
if ! txs . contains_key ( & txid ) {
let tx_entry = WalletTx ::new ( height , & txid ) ;
let tx_entry = WalletTx ::new ( height , timestamp , & txid ) ;
txs . insert ( txid . clone ( ) , tx_entry ) ;
}
let tx_entry = txs . get_mut ( & txid ) . unwrap ( ) ;
@ -600,7 +845,7 @@ impl LightWallet {
}
// Scan the full Tx and update memos for incoming shielded transactions
pub fn scan_full_tx ( & self , tx : & Transaction , height : i32 ) {
pub fn scan_full_tx ( & self , tx : & Transaction , height : i32 , datetime : u64 ) {
// Scan all the inputs to see if we spent any transparent funds in this tx
// TODO: Save this object
@ -639,7 +884,7 @@ impl LightWallet {
let mut txs = self . txs . write ( ) . unwrap ( ) ;
if ! txs . contains_key ( & tx . txid ( ) ) {
let tx_entry = WalletTx ::new ( height , & tx . txid ( ) ) ;
let tx_entry = WalletTx ::new ( height , datetime , & tx . txid ( ) ) ;
txs . insert ( tx . txid ( ) . clone ( ) , tx_entry ) ;
}
@ -661,7 +906,7 @@ impl LightWallet {
Some ( TransparentAddress ::PublicKey ( hash ) ) = > {
if hash [ . . ] = = ripemd160 ::Ripemd160 ::digest ( & Sha256 ::digest ( & pubkey ) ) [ . . ] {
// This is our address. Add this as an output to the txid
self . add_toutput_to_wtx ( height , & tx . txid ( ) , & vout , n as u64 ) ;
self . add_toutput_to_wtx ( height , datetime , & tx . txid ( ) , & vout , n as u64 ) ;
}
} ,
_ = > { }
@ -676,8 +921,8 @@ impl LightWallet {
// outgoing metadata
// Collect our t-addresses
let wallet_taddrs = self . tkey s . read ( ) . unwrap ( ) . iter ( )
. map ( | sk | self . address_from_sk ( sk ) )
let wallet_taddrs = self . taddresse s . read ( ) . unwrap ( ) . iter ( )
. map ( | a | a . clone ( ) )
. collect ::< HashSet < String > > ( ) ;
for vout in tx . vout . iter ( ) {
@ -745,7 +990,7 @@ impl LightWallet {
// First, collect all our z addresses, to check for change
// Collect z addresses
let z_addresses = self . address . read ( ) . unwrap ( ) . iter ( ) . map ( | ad | {
let z_addresses = self . z address. read ( ) . unwrap ( ) . iter ( ) . map ( | ad | {
encode_payment_address ( self . config . hrp_sapling_address ( ) , & ad )
} ) . collect ::< HashSet < String > > ( ) ;
@ -1013,7 +1258,7 @@ impl LightWallet {
// Find the existing transaction entry, or create a new one.
if ! txs . contains_key ( & tx . txid ) {
let tx_entry = WalletTx ::new ( block_data . height as i32 , & tx . txid ) ;
let tx_entry = WalletTx ::new ( block_data . height as i32 , block . time as u64 , & tx . txid ) ;
txs . insert ( tx . txid , tx_entry ) ;
}
let tx_entry = txs . get_mut ( & tx . txid ) . unwrap ( ) ;
@ -1065,6 +1310,10 @@ impl LightWallet {
output_params : & [ u8 ] ,
tos : Vec < ( & str , u64 , Option < String > ) >
) -> Result < Box < [ u8 ] > , String > {
if ! self . unlocked {
return Err ( "Cannot spend while wallet is locked" . to_string ( ) ) ;
}
let start_time = now ( ) ;
let total_value = tos . iter ( ) . map ( | to | to . 1 ) . sum ::< u64 > ( ) ;
@ -1175,7 +1424,7 @@ impl LightWallet {
if selected_value < u64 ::from ( target_value ) {
let e = format ! (
"Insufficient verified funds (have {}, need {:?}).\nNote : funds need {} confirmations before they can be spent" ,
"Insufficient verified funds (have {}, need {:?}). NOTE : funds need {} confirmations before they can be spent. " ,
selected_value , target_value , self . config . anchor_offset
) ;
error ! ( "{}" , e ) ;
@ -1669,7 +1918,7 @@ pub mod tests {
tx . add_t_output ( & pk , AMOUNT1 ) ;
let txid1 = tx . get_tx ( ) . txid ( ) ;
wallet . scan_full_tx ( & tx . get_tx ( ) , 100 ) ; // Pretend it is at height 100
wallet . scan_full_tx ( & tx . get_tx ( ) , 100 , 0 ) ; // Pretend it is at height 100
{
let txs = wallet . txs . read ( ) . unwrap ( ) ;
@ -1694,7 +1943,7 @@ pub mod tests {
tx . add_t_input ( txid1 , 0 ) ;
let txid2 = tx . get_tx ( ) . txid ( ) ;
wallet . scan_full_tx ( & tx . get_tx ( ) , 101 ) ; // Pretent it is at height 101
wallet . scan_full_tx ( & tx . get_tx ( ) , 101 , 0 ) ; // Pretent it is at height 101
{
// Make sure the txid was spent
@ -1741,7 +1990,7 @@ pub mod tests {
tx . add_t_output ( & non_wallet_pk , 25 ) ;
let txid1 = tx . get_tx ( ) . txid ( ) ;
wallet . scan_full_tx ( & tx . get_tx ( ) , 100 ) ; // Pretend it is at height 100
wallet . scan_full_tx ( & tx . get_tx ( ) , 100 , 0 ) ; // Pretend it is at height 100
{
let txs = wallet . txs . read ( ) . unwrap ( ) ;
@ -1766,7 +2015,7 @@ pub mod tests {
tx . add_t_input ( txid1 , 1 ) ; // Ours was at position 1 in the input tx
let txid2 = tx . get_tx ( ) . txid ( ) ;
wallet . scan_full_tx ( & tx . get_tx ( ) , 101 ) ; // Pretent it is at height 101
wallet . scan_full_tx ( & tx . get_tx ( ) , 101 , 0 ) ; // Pretent it is at height 101
{
// Make sure the txid was spent
@ -1815,7 +2064,7 @@ pub mod tests {
let mut tx = FakeTransaction ::new_with_txid ( txid1 ) ;
tx . add_t_output ( & pk , TAMOUNT1 ) ;
wallet . scan_full_tx ( & tx . get_tx ( ) , 0 ) ; // Height 0
wallet . scan_full_tx ( & tx . get_tx ( ) , 0 , 0 ) ; // Height 0
const AMOUNT2 :u64 = 2 ;
@ -1828,7 +2077,7 @@ pub mod tests {
let mut tx = FakeTransaction ::new_with_txid ( txid2 ) ;
tx . add_t_input ( txid1 , 0 ) ;
wallet . scan_full_tx ( & tx . get_tx ( ) , 1 ) ; // Height 1
wallet . scan_full_tx ( & tx . get_tx ( ) , 1 , 0 ) ; // Height 1
// Now, the original note should be spent and there should be a change
assert_eq ! ( wallet . zbalance ( None ) , AMOUNT1 - AMOUNT2 ) ; // The t addr amount is received + spent, so it cancels out
@ -1847,7 +2096,7 @@ pub mod tests {
assert_eq ! ( wallet . extsks . read ( ) . unwrap ( ) . len ( ) , wallet2 . extsks . read ( ) . unwrap ( ) . len ( ) ) ;
assert_eq ! ( wallet . extsks . read ( ) . unwrap ( ) [ 0 ] , wallet2 . extsks . read ( ) . unwrap ( ) [ 0 ] ) ;
assert_eq ! ( wallet . extfvks . read ( ) . unwrap ( ) [ 0 ] , wallet2 . extfvks . read ( ) . unwrap ( ) [ 0 ] ) ;
assert_eq ! ( wallet . address . read ( ) . unwrap ( ) [ 0 ] , wallet2 . address . read ( ) . unwrap ( ) [ 0 ] ) ;
assert_eq ! ( wallet . z address. read ( ) . unwrap ( ) [ 0 ] , wallet2 . z address. read ( ) . unwrap ( ) [ 0 ] ) ;
assert_eq ! ( wallet . tkeys . read ( ) . unwrap ( ) . len ( ) , wallet2 . tkeys . read ( ) . unwrap ( ) . len ( ) ) ;
assert_eq ! ( wallet . tkeys . read ( ) . unwrap ( ) [ 0 ] , wallet2 . tkeys . read ( ) . unwrap ( ) [ 0 ] ) ;
@ -1916,7 +2165,7 @@ pub mod tests {
assert_eq ! ( wallet2 . tkeys . read ( ) . unwrap ( ) . len ( ) , 2 ) ;
assert_eq ! ( wallet2 . extsks . read ( ) . unwrap ( ) . len ( ) , 2 ) ;
assert_eq ! ( wallet2 . extfvks . read ( ) . unwrap ( ) . len ( ) , 2 ) ;
assert_eq ! ( wallet2 . address . read ( ) . unwrap ( ) . len ( ) , 2 ) ;
assert_eq ! ( wallet2 . z address. read ( ) . unwrap ( ) . len ( ) , 2 ) ;
assert_eq ! ( taddr1 , wallet . address_from_sk ( & wallet . tkeys . read ( ) . unwrap ( ) [ 0 ] ) ) ;
assert_eq ! ( taddr2 , wallet . address_from_sk ( & wallet . tkeys . read ( ) . unwrap ( ) [ 1 ] ) ) ;
@ -2023,7 +2272,7 @@ pub mod tests {
}
// Now, full scan the Tx, which should populate the Outgoing Meta data
wallet . scan_full_tx ( & sent_tx , 2 ) ;
wallet . scan_full_tx ( & sent_tx , 2 , 0 ) ;
// Check Outgoing Metadata
{
@ -2063,7 +2312,7 @@ pub mod tests {
let mut cb3 = FakeCompactBlock ::new ( 2 , block_hash ) ;
cb3 . add_tx ( & sent_tx ) ;
wallet . scan_block ( & cb3 . as_bytes ( ) ) . unwrap ( ) ;
wallet . scan_full_tx ( & sent_tx , 2 ) ;
wallet . scan_full_tx ( & sent_tx , 2 , 0 ) ;
// Because the builder will randomize notes outputted, we need to find
// which note number is the change and which is the output note (Because this tx
@ -2116,7 +2365,7 @@ pub mod tests {
let mut cb4 = FakeCompactBlock ::new ( 3 , cb3 . hash ( ) ) ;
cb4 . add_tx ( & sent_tx ) ;
wallet . scan_block ( & cb4 . as_bytes ( ) ) . unwrap ( ) ;
wallet . scan_full_tx ( & sent_tx , 3 ) ;
wallet . scan_full_tx ( & sent_tx , 3 , 0 ) ;
{
// Both notes should be spent now.
@ -2187,7 +2436,7 @@ pub mod tests {
}
// Now, full scan the Tx, which should populate the Outgoing Meta data
wallet . scan_full_tx ( & sent_tx , 2 ) ;
wallet . scan_full_tx ( & sent_tx , 2 , 0 ) ;
// Check Outgoing Metadata for t address
{
@ -2215,7 +2464,7 @@ pub mod tests {
tx . add_t_output ( & pk , AMOUNT_T ) ;
let txid_t = tx . get_tx ( ) . txid ( ) ;
wallet . scan_full_tx ( & tx . get_tx ( ) , 1 ) ; // Pretend it is at height 1
wallet . scan_full_tx ( & tx . get_tx ( ) , 1 , 0 ) ; // Pretend it is at height 1
{
let txs = wallet . txs . read ( ) . unwrap ( ) ;
@ -2277,7 +2526,7 @@ pub mod tests {
// Scan the compact block and the full Tx
wallet . scan_block ( & cb3 . as_bytes ( ) ) . unwrap ( ) ;
wallet . scan_full_tx ( & sent_tx , 2 ) ;
wallet . scan_full_tx ( & sent_tx , 2 , 0 ) ;
// Now this new Spent tx should be in, so the note should be marked confirmed spent
{
@ -2327,7 +2576,7 @@ pub mod tests {
wallet . scan_block ( & cb3 . as_bytes ( ) ) . unwrap ( ) ;
// And scan the Full Tx to get the memo
wallet . scan_full_tx ( & sent_tx , 2 ) ;
wallet . scan_full_tx ( & sent_tx , 2 , 0 ) ;
{
let txs = wallet . txs . read ( ) . unwrap ( ) ;
@ -2336,7 +2585,7 @@ pub mod tests {
assert_eq ! ( txs [ & sent_txid ] . notes [ 0 ] . extfvk , wallet . extfvks . read ( ) . unwrap ( ) [ 0 ] ) ;
assert_eq ! ( txs [ & sent_txid ] . notes [ 0 ] . note . value , AMOUNT1 - fee ) ;
assert_eq ! ( wallet . note_address ( & txs [ & sent_txid ] . notes [ 0 ] ) , Some ( my_address ) ) ;
assert_eq ! ( LightWallet ::note_address ( wallet . config . hrp_sapling_address ( ) , & txs [ & sent_txid ] . notes [ 0 ] ) , Some ( my_address ) ) ;
assert_eq ! ( LightWallet ::memo_str ( & txs [ & sent_txid ] . notes [ 0 ] . memo ) , Some ( memo ) ) ;
}
}
@ -2366,7 +2615,7 @@ pub mod tests {
wallet . scan_block ( & cb3 . as_bytes ( ) ) . unwrap ( ) ;
// And scan the Full Tx to get the memo
wallet . scan_full_tx ( & sent_tx , 2 ) ;
wallet . scan_full_tx ( & sent_tx , 2 , 0 ) ;
{
let txs = wallet . txs . read ( ) . unwrap ( ) ;
@ -2424,7 +2673,7 @@ pub mod tests {
let mut cb3 = FakeCompactBlock ::new ( 2 , block_hash ) ;
cb3 . add_tx ( & sent_tx ) ;
wallet . scan_block ( & cb3 . as_bytes ( ) ) . unwrap ( ) ;
wallet . scan_full_tx ( & sent_tx , 2 ) ;
wallet . scan_full_tx ( & sent_tx , 2 , 0 ) ;
// Check that the send to the second taddr worked
{
@ -2468,7 +2717,7 @@ pub mod tests {
let mut cb4 = FakeCompactBlock ::new ( 3 , cb3 . hash ( ) ) ;
cb4 . add_tx ( & sent_tx ) ;
wallet . scan_block ( & cb4 . as_bytes ( ) ) . unwrap ( ) ;
wallet . scan_full_tx ( & sent_tx , 3 ) ;
wallet . scan_full_tx ( & sent_tx , 3 , 0 ) ;
// Quickly check we have it
{
@ -2505,7 +2754,7 @@ pub mod tests {
let mut cb5 = FakeCompactBlock ::new ( 4 , cb4 . hash ( ) ) ;
cb5 . add_tx ( & sent_tx ) ;
wallet . scan_block ( & cb5 . as_bytes ( ) ) . unwrap ( ) ;
wallet . scan_full_tx ( & sent_tx , 4 ) ;
wallet . scan_full_tx ( & sent_tx , 4 , 0 ) ;
{
let txs = wallet . txs . read ( ) . unwrap ( ) ;
@ -2561,7 +2810,7 @@ pub mod tests {
let mut cb3 = FakeCompactBlock ::new ( 2 , block_hash ) ;
cb3 . add_tx ( & sent_tx ) ;
wallet . scan_block ( & cb3 . as_bytes ( ) ) . unwrap ( ) ;
wallet . scan_full_tx ( & sent_tx , 2 ) ;
wallet . scan_full_tx ( & sent_tx , 2 , 0 ) ;
// Make sure all the outputs are there!
{
@ -2633,7 +2882,7 @@ pub mod tests {
let mut cb4 = FakeCompactBlock ::new ( 3 , cb3 . hash ( ) ) ;
cb4 . add_tx ( & sent_tx ) ;
wallet . scan_block ( & cb4 . as_bytes ( ) ) . unwrap ( ) ;
wallet . scan_full_tx ( & sent_tx , 3 ) ;
wallet . scan_full_tx ( & sent_tx , 3 , 0 ) ;
// Make sure all the outputs are there!
{
@ -2811,7 +3060,7 @@ pub mod tests {
let mut cb3 = FakeCompactBlock ::new ( 7 , blk6_hash ) ;
cb3 . add_tx ( & sent_tx ) ;
wallet . scan_block ( & cb3 . as_bytes ( ) ) . unwrap ( ) ;
wallet . scan_full_tx ( & sent_tx , 7 ) ;
wallet . scan_full_tx ( & sent_tx , 7 , 0 ) ;
// Make sure the Tx is in.
{
@ -2868,9 +3117,15 @@ pub mod tests {
// Test the addresses against https://iancoleman.io/bip39/
let ( taddr , pk ) = & wallet . get_t_secret_keys ( ) [ 0 ] ;
assert_eq ! ( taddr , "RVog7rQu2Zo2iAQCjbZGXsiQm7SYr9bcaq " ) ;
assert_eq ! ( taddr , "t1eQ63fwkQ4n4Eo5uCrPGaAV8FWB2tmx7ui " ) ;
assert_eq ! ( pk , "Kz9ybX4giKag4NtnP1pi8WQF2B2hZDkFU85S7Dciz3UUhM59AnhE" ) ;
// Test a couple more
wallet . add_taddr ( ) ;
let ( taddr , pk ) = & wallet . get_t_secret_keys ( ) [ 1 ] ;
assert_eq ! ( taddr , "t1NoS6ZgaUTpmjkge2cVpXGcySasdYDrXqh" ) ;
assert_eq ! ( pk , "KxdmS38pxskS6bbKX43zhTu8ppWckNmWjKsQFX1hwidvhRRgRd3c" ) ;
let ( zaddr , sk ) = & wallet . get_z_private_keys ( ) [ 0 ] ;
assert_eq ! ( zaddr , "zs1q6xk3q783t5k92kjqt2rkuuww8pdw2euzy5rk6jytw97enx8fhpazdv3th4xe7vsk6e9sfpawfg" ) ;
assert_eq ! ( sk , "secret-extended-key-main1qvpa0qr8qqqqpqxn4l054nzxpxzp3a8r2djc7sekdek5upce8mc2j2z0arzps4zv940qeg706hd0wq6g5snzvhp332y6vhwyukdn8dhekmmsk7fzvzkqm6ypc99uy63tpesqwxhpre78v06cx8k5xpp9mrhtgqs5dvp68cqx2yrvthflmm2ynl8c0506dekul0f6jkcdmh0292lpphrksyc5z3pxwws97zd5els3l2mjt2s7hntap27mlmt6w0drtfmz36vz8pgu7ec0twfrq" ) ;
@ -2878,9 +3133,112 @@ pub mod tests {
assert_eq ! ( seed_phrase , Some ( wallet . get_seed_phrase ( ) ) ) ;
}
#[ test ]
fn test_lock_unlock ( ) {
const AMOUNT : u64 = 500000 ;
let ( mut wallet , _ , _ ) = get_test_wallet ( AMOUNT ) ;
let config = wallet . config . clone ( ) ;
// Add some addresses
let zaddr0 = encode_payment_address ( config . hrp_sapling_address ( ) ,
& wallet . extfvks . read ( ) . unwrap ( ) [ 0 ] . default_address ( ) . unwrap ( ) . 1 ) ;
let zaddr1 = wallet . add_zaddr ( ) ;
let zaddr2 = wallet . add_zaddr ( ) ;
let taddr0 = wallet . address_from_sk ( & wallet . tkeys . read ( ) . unwrap ( ) [ 0 ] ) ;
let taddr1 = wallet . add_taddr ( ) ;
let taddr2 = wallet . add_taddr ( ) ;
let seed = wallet . seed ;
wallet . encrypt ( "somepassword" . to_string ( ) ) . unwrap ( ) ;
// Encrypting an already encrypted wallet should fail
assert ! ( wallet . encrypt ( "somepassword" . to_string ( ) ) . is_err ( ) ) ;
// Serialize a locked wallet
let mut serialized_data = vec ! [ ] ;
wallet . write ( & mut serialized_data ) . expect ( "Serialize wallet" ) ;
// Should fail when there's a wrong password
assert ! ( wallet . unlock ( "differentpassword" . to_string ( ) ) . is_err ( ) ) ;
// Properly unlock
wallet . unlock ( "somepassword" . to_string ( ) ) . unwrap ( ) ;
assert_eq ! ( seed , wallet . seed ) ;
{
let extsks = wallet . extsks . read ( ) . unwrap ( ) ;
let tkeys = wallet . tkeys . read ( ) . unwrap ( ) ;
assert_eq ! ( extsks . len ( ) , 3 ) ;
assert_eq ! ( tkeys . len ( ) , 3 ) ;
assert_eq ! ( zaddr0 , encode_payment_address ( config . hrp_sapling_address ( ) ,
& ExtendedFullViewingKey ::from ( & extsks [ 0 ] ) . default_address ( ) . unwrap ( ) . 1 ) ) ;
assert_eq ! ( zaddr1 , encode_payment_address ( config . hrp_sapling_address ( ) ,
& ExtendedFullViewingKey ::from ( & extsks [ 1 ] ) . default_address ( ) . unwrap ( ) . 1 ) ) ;
assert_eq ! ( zaddr2 , encode_payment_address ( config . hrp_sapling_address ( ) ,
& ExtendedFullViewingKey ::from ( & extsks [ 2 ] ) . default_address ( ) . unwrap ( ) . 1 ) ) ;
assert_eq ! ( taddr0 , wallet . address_from_sk ( & tkeys [ 0 ] ) ) ;
assert_eq ! ( taddr1 , wallet . address_from_sk ( & tkeys [ 1 ] ) ) ;
assert_eq ! ( taddr2 , wallet . address_from_sk ( & tkeys [ 2 ] ) ) ;
}
// Unlocking an already unlocked wallet should fail
assert ! ( wallet . unlock ( "somepassword" . to_string ( ) ) . is_err ( ) ) ;
// Trying to serialize a encrypted but unlocked wallet should fail
assert ! ( wallet . write ( & mut vec ! [ ] ) . is_err ( ) ) ;
// ...but if we lock it again, it should serialize
wallet . lock ( ) . unwrap ( ) ;
wallet . write ( & mut vec ! [ ] ) . expect ( "Serialize wallet" ) ;
// Try from a deserialized, locked wallet
let mut wallet2 = LightWallet ::read ( & serialized_data [ . . ] , & config ) . unwrap ( ) ;
wallet2 . unlock ( "somepassword" . to_string ( ) ) . unwrap ( ) ;
assert_eq ! ( seed , wallet2 . seed ) ;
{
let extsks = wallet2 . extsks . read ( ) . unwrap ( ) ;
let tkeys = wallet2 . tkeys . read ( ) . unwrap ( ) ;
assert_eq ! ( extsks . len ( ) , 3 ) ;
assert_eq ! ( tkeys . len ( ) , 3 ) ;
assert_eq ! ( zaddr0 , encode_payment_address ( wallet2 . config . hrp_sapling_address ( ) ,
& ExtendedFullViewingKey ::from ( & extsks [ 0 ] ) . default_address ( ) . unwrap ( ) . 1 ) ) ;
assert_eq ! ( zaddr1 , encode_payment_address ( wallet2 . config . hrp_sapling_address ( ) ,
& ExtendedFullViewingKey ::from ( & extsks [ 1 ] ) . default_address ( ) . unwrap ( ) . 1 ) ) ;
assert_eq ! ( zaddr2 , encode_payment_address ( wallet2 . config . hrp_sapling_address ( ) ,
& ExtendedFullViewingKey ::from ( & extsks [ 2 ] ) . default_address ( ) . unwrap ( ) . 1 ) ) ;
assert_eq ! ( taddr0 , wallet2 . address_from_sk ( & tkeys [ 0 ] ) ) ;
assert_eq ! ( taddr1 , wallet2 . address_from_sk ( & tkeys [ 1 ] ) ) ;
assert_eq ! ( taddr2 , wallet2 . address_from_sk ( & tkeys [ 2 ] ) ) ;
}
}
#[ test ]
#[ should_panic ]
fn test_invalid_bip39_t ( ) {
// Passing a 32-byte seed to bip32 should fail.
let config = get_test_config ( ) ;
LightWallet ::get_taddr_from_bip39seed ( & config , & [ 0 u8 ; 32 ] , 0 ) ;
}
#[ test ]
#[ should_panic ]
fn test_invalid_bip39_z ( ) {
// Passing a 32-byte seed to bip32 should fail.
let config = get_test_config ( ) ;
LightWallet ::get_zaddr_from_bip39seed ( & config , & [ 0 u8 ; 32 ] , 0 ) ;
}
#[ test ]
fn test_invalid_scan_blocks ( ) {
const AMOUNT : u64 = 50000 ;
const AMOUNT : u64 = 500000 ;
let ( wallet , _txid1 , block_hash ) = get_test_wallet ( AMOUNT ) ;
let prev_hash = add_blocks ( & wallet , 2 , 1 , block_hash ) . unwrap ( ) ;