diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 796878c..abc5ddd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -8,7 +8,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + os: [ubuntu-16.04, windows-latest, macOS-latest] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/rust_BACKUP_5752.yml b/.github/workflows/rust_BACKUP_5752.yml new file mode 100644 index 0000000..796878c --- /dev/null +++ b/.github/workflows/rust_BACKUP_5752.yml @@ -0,0 +1,104 @@ +name: Rust + +on: [push, pull_request] + +jobs: + build: + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.38.0 + override: true + - name: cargo fetch + uses: actions-rs/cargo@v1 + with: + command: fetch + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --verbose --release --all + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --verbose --release --all + - name: Upload ubuntu/macos + uses: actions/upload-artifact@v1 + if: contains(matrix.os, 'macos') || contains(matrix.os, 'ubuntu') + with: +<<<<<<< HEAD +<<<<<<< HEAD + name: ${{ matrix.os }}-silentdragonlite-cli + path: target/release/silentdragonlite-cli +======= + name: ${{ matrix.os }}-silentdragonlite-cli + path: target/release/silentdragonlite-cli +======= + name: ${{ matrix.os }}-zecwallet-cli + path: target/release/zecwallet-cli +>>>>>>> 959755d705b18d13a8dbdf0502c47818fe7a4e94 + - name: Upload windows + uses: actions/upload-artifact@v1 + if: contains(matrix.os, 'windows') + with: +<<<<<<< HEAD + name: ${{ matrix.os }}-silentdragonlite-cli.exe + path: target/release/silentdragonlite-cli.exe + +>>>>>>> 959755d705b18d13a8dbdf0502c47818fe7a4e94 +======= + name: ${{ matrix.os }}-zecwallet-cli.exe + path: target/release/zecwallet-cli.exe + +>>>>>>> 959755d705b18d13a8dbdf0502c47818fe7a4e94 + + linux_arm7: + name: Linux ARMv7 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: armv7-unknown-linux-gnueabihf + override: true + - uses: actions-rs/cargo@v1 + with: + use-cross: true + command: build + args: --release --target armv7-unknown-linux-gnueabihf + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: linux_armv7-silentdragonlite-cli + path: target/armv7-unknown-linux-gnueabihf/release/silentdragonlite-cli + + linux_aarch64: + name: Linux ARM64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: aarch64-unknown-linux-gnu + override: true + - uses: actions-rs/cargo@v1 + with: + use-cross: true + command: build + args: --release --target aarch64-unknown-linux-gnu + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: linux_aarch64-silentdragonlite-cli + path: target/aarch64-unknown-linux-gnu/release/silentdragonlite-cli + diff --git a/.github/workflows/rust_BASE_5752.yml b/.github/workflows/rust_BASE_5752.yml new file mode 100644 index 0000000..422cf60 --- /dev/null +++ b/.github/workflows/rust_BASE_5752.yml @@ -0,0 +1,80 @@ +name: Rust + +on: [push, pull_request] + +jobs: + build: + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.38.0 + override: true + - name: cargo fetch + uses: actions-rs/cargo@v1 + with: + command: fetch + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --verbose --release --all + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --verbose --release --all + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: ${{ matrix.os }}-zecwallet-cli + path: target/release/zecwallet-cli + + linux_arm7: + name: Linux ARMv7 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: armv7-unknown-linux-gnueabihf + override: true + - uses: actions-rs/cargo@v1 + with: + use-cross: true + command: build + args: --release --target armv7-unknown-linux-gnueabihf + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: linux_armv7-zecwallet-cli + path: target/armv7-unknown-linux-gnueabihf/release/zecwallet-cli + + linux_aarch64: + name: Linux ARM64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: aarch64-unknown-linux-gnu + override: true + - uses: actions-rs/cargo@v1 + with: + use-cross: true + command: build + args: --release --target aarch64-unknown-linux-gnu + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: linux_aarch64-zecwallet-cli + path: target/aarch64-unknown-linux-gnu/release/zecwallet-cli + diff --git a/.github/workflows/rust_LOCAL_5752.yml b/.github/workflows/rust_LOCAL_5752.yml new file mode 100644 index 0000000..f61dc33 --- /dev/null +++ b/.github/workflows/rust_LOCAL_5752.yml @@ -0,0 +1,93 @@ +name: Rust + +on: [push, pull_request] + +jobs: + build: + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.38.0 + override: true + - name: cargo fetch + uses: actions-rs/cargo@v1 + with: + command: fetch + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --verbose --release --all + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --verbose --release --all + - name: Upload ubuntu/macos + uses: actions/upload-artifact@v1 + if: contains(matrix.os, 'macos') || contains(matrix.os, 'ubuntu') + with: +<<<<<<< HEAD + name: ${{ matrix.os }}-silentdragonlite-cli + path: target/release/silentdragonlite-cli +======= + name: ${{ matrix.os }}-silentdragonlite-cli + path: target/release/silentdragonlite-cli + - name: Upload windows + uses: actions/upload-artifact@v1 + if: contains(matrix.os, 'windows') + with: + name: ${{ matrix.os }}-silentdragonlite-cli.exe + path: target/release/silentdragonlite-cli.exe + +>>>>>>> 959755d705b18d13a8dbdf0502c47818fe7a4e94 + + linux_arm7: + name: Linux ARMv7 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: armv7-unknown-linux-gnueabihf + override: true + - uses: actions-rs/cargo@v1 + with: + use-cross: true + command: build + args: --release --target armv7-unknown-linux-gnueabihf + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: linux_armv7-silentdragonlite-cli + path: target/armv7-unknown-linux-gnueabihf/release/silentdragonlite-cli + + linux_aarch64: + name: Linux ARM64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: aarch64-unknown-linux-gnu + override: true + - uses: actions-rs/cargo@v1 + with: + use-cross: true + command: build + args: --release --target aarch64-unknown-linux-gnu + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: linux_aarch64-silentdragonlite-cli + path: target/aarch64-unknown-linux-gnu/release/silentdragonlite-cli + diff --git a/.github/workflows/rust_REMOTE_5752.yml b/.github/workflows/rust_REMOTE_5752.yml new file mode 100644 index 0000000..3f634ac --- /dev/null +++ b/.github/workflows/rust_REMOTE_5752.yml @@ -0,0 +1,88 @@ +name: Rust + +on: [push, pull_request] + +jobs: + build: + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.38.0 + override: true + - name: cargo fetch + uses: actions-rs/cargo@v1 + with: + command: fetch + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --verbose --release --all + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --verbose --release --all + - name: Upload ubuntu/macos + uses: actions/upload-artifact@v1 + if: contains(matrix.os, 'macos') || contains(matrix.os, 'ubuntu') + with: + name: ${{ matrix.os }}-zecwallet-cli + path: target/release/zecwallet-cli + - name: Upload windows + uses: actions/upload-artifact@v1 + if: contains(matrix.os, 'windows') + with: + name: ${{ matrix.os }}-zecwallet-cli.exe + path: target/release/zecwallet-cli.exe + + + linux_arm7: + name: Linux ARMv7 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: armv7-unknown-linux-gnueabihf + override: true + - uses: actions-rs/cargo@v1 + with: + use-cross: true + command: build + args: --release --target armv7-unknown-linux-gnueabihf + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: linux_armv7-zecwallet-cli + path: target/armv7-unknown-linux-gnueabihf/release/zecwallet-cli + + linux_aarch64: + name: Linux ARM64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: aarch64-unknown-linux-gnu + override: true + - uses: actions-rs/cargo@v1 + with: + use-cross: true + command: build + args: --release --target aarch64-unknown-linux-gnu + - name: Upload + uses: actions/upload-artifact@v1 + with: + name: linux_aarch64-zecwallet-cli + path: target/aarch64-unknown-linux-gnu/release/zecwallet-cli + diff --git a/cli/src/main.rs b/cli/src/main.rs index 43d3d28..3a98157 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -61,6 +61,11 @@ pub fn main() { .value_name("seed_phrase") .help("Create a new wallet with the given 24-word seed phrase. Will fail if wallet already exists") .takes_value(true)) + .arg(Arg::with_name("birthday") + .long("birthday") + .value_name("birthday") + .help("Specify wallet birthday when restoring from seed. This is the earlist block height where the wallet has a transaction.") + .takes_value(true)) .arg(Arg::with_name("server") .long("server") .value_name("server") @@ -112,8 +117,25 @@ pub fn main() { let command = matches.value_of("COMMAND"); let params = matches.values_of("PARAMS").map(|v| v.collect()).or(Some(vec![])).unwrap(); - let maybe_server = matches.value_of("server").map(|s| s.to_string()); - let seed = matches.value_of("seed").map(|s| s.to_string()); + let maybe_server = matches.value_of("server").map(|s| s.to_string()); + + let seed = matches.value_of("seed").map(|s| s.to_string()); + let maybe_birthday = matches.value_of("birthday"); + + if seed.is_some() && maybe_birthday.is_none() { + eprintln!("ERROR!"); + eprintln!("Please specify the wallet birthday (eg. '--birthday 600000') to restore from seed."); + eprintln!("This should be the block height where the wallet was created. If you don't remember the block height, you can pass '--birthday 0' to scan from the start of the blockchain."); + return; + } + + let birthday = match maybe_birthday.unwrap_or("0").parse::() { + Ok(b) => b, + Err(e) => { + eprintln!("Couldn't parse birthday. This should be a block number. Error={}", e); + return; + } + }; let server = LightClientConfig::get_server_or_default(maybe_server); @@ -125,7 +147,7 @@ pub fn main() { let dangerous = matches.is_present("dangerous"); let nosync = matches.is_present("nosync"); - let (command_tx, resp_rx) = match startup(server, dangerous, seed, !nosync, command.is_none()) { + let (command_tx, resp_rx) = match startup(server, dangerous, seed, birthday, !nosync, command.is_none()) { Ok(c) => c, Err(e) => { eprintln!("Error during startup: {}", e); @@ -163,7 +185,7 @@ pub fn main() { } } -fn startup(server: http::Uri, dangerous: bool, seed: Option, first_sync: bool, print_updates: bool) +fn startup(server: http::Uri, dangerous: bool, seed: Option, birthday: u64, first_sync: bool, print_updates: bool) -> io::Result<(Sender<(String, Vec)>, Receiver)> { // Try to get the configuration let (config, latest_block_height) = LightClientConfig::create(server.clone(), dangerous)?; @@ -175,7 +197,7 @@ fn startup(server: http::Uri, dangerous: bool, seed: Option, first_sync: })?; let lightclient = match seed { - Some(phrase) => Arc::new(LightClient::new_from_phrase(phrase, &config, latest_block_height)?), + Some(phrase) => Arc::new(LightClient::new_from_phrase(phrase, &config, birthday)?), None => { if config.wallet_exists() { Arc::new(LightClient::read_from_disk(&config)?) @@ -195,9 +217,6 @@ fn startup(server: http::Uri, dangerous: bool, seed: Option, first_sync: println!("Lightclient connecting to {}", config.server); } - // Start the command loop - let (command_tx, resp_rx) = command_loop(lightclient.clone()); - // At startup, run a sync. if first_sync { let update = lightclient.do_sync(true); @@ -206,6 +225,9 @@ fn startup(server: http::Uri, dangerous: bool, seed: Option, first_sync: } } + // Start the command loop + let (command_tx, resp_rx) = command_loop(lightclient.clone()); + Ok((command_tx, resp_rx)) } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index ff6f8d9..640aabd 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -3,7 +3,7 @@ use crate::lightwallet::LightWallet; use log::{info, warn, error}; use rand::{rngs::OsRng, seq::SliceRandom}; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, RwLock, Mutex}; use std::sync::atomic::{AtomicU64, AtomicI32, AtomicUsize, Ordering}; use std::path::{Path, PathBuf}; use std::fs::File; @@ -25,6 +25,8 @@ use crate::grpcconnector::{self, *}; use crate::SaplingParams; use crate::ANCHOR_OFFSET; +mod checkpoints; + pub const DEFAULT_SERVER: &str = "https://"; pub const WALLET_NAME: &str = "silentdragonlite-cli-wallet.dat"; pub const LOGFILE_NAME: &str = "silentdragonlite-cli-wallet.debug.log"; @@ -117,18 +119,8 @@ impl LightClientConfig { log_path.into_boxed_path() } - pub fn get_initial_state(&self) -> Option<(u64, &str, &str)> { - match &self.chain_name[..] { - "test" => Some((105942, - "00000001c0199f329ee03379bf1387856dbab23765da508bf9b9d8d544f212c0", - "" - )), - "main" => Some((105944, - "0000000313b0ec7c5a1e9b997ce44a7763b56c5505526c36634a004ed52d7787", - "" - )), - _ => None - } + pub fn get_initial_state(&self, height: u64) -> Option<(u64, &str, &str)> { + checkpoints::get_closest_checkpoint(&self.chain_name, height) } pub fn get_server_or_default(server: Option) -> http::Uri { @@ -209,14 +201,16 @@ pub struct LightClient { // zcash-params pub sapling_output : Vec, pub sapling_spend : Vec, + + sync_lock : Mutex<()>, } impl LightClient { - pub fn set_wallet_initial_state(&self) { + pub fn set_wallet_initial_state(&self, height: u64) { use std::convert::TryInto; - let state = self.config.get_initial_state(); + let state = self.config.get_initial_state(height); match state { Some((height, hash, tree)) => self.wallet.read().unwrap().set_initial_block(height.try_into().unwrap(), hash, tree), @@ -239,10 +233,11 @@ impl LightClient { wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), &config, 0)?)), config : config.clone(), sapling_output : vec![], - sapling_spend : vec![] + sapling_spend : vec![], + sync_lock : Mutex::new(()), }; - l.set_wallet_initial_state(); + l.set_wallet_initial_state(0); l.read_sapling_params(); info!("Created new wallet!"); @@ -263,10 +258,11 @@ impl LightClient { wallet : Arc::new(RwLock::new(LightWallet::new(None, config, latest_block)?)), config : config.clone(), sapling_output : vec![], - sapling_spend : vec![] + sapling_spend : vec![], + sync_lock : Mutex::new(()), }; - l.set_wallet_initial_state(); + l.set_wallet_initial_state(latest_block); l.read_sapling_params(); info!("Created new wallet with a new seed!"); @@ -275,20 +271,22 @@ impl LightClient { Ok(l) } - pub fn new_from_phrase(seed_phrase: String, config: &LightClientConfig, latest_block: u64) -> io::Result { + pub fn new_from_phrase(seed_phrase: String, config: &LightClientConfig, birthday: u64) -> io::Result { if config.wallet_exists() { return Err(Error::new(ErrorKind::AlreadyExists, "Cannot create a new wallet from seed, because a wallet already exists")); } let mut l = LightClient { - wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), config, latest_block)?)), + wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), config, birthday)?)), config : config.clone(), sapling_output : vec![], - sapling_spend : vec![] + sapling_spend : vec![], + sync_lock : Mutex::new(()), }; - l.set_wallet_initial_state(); + println!("Setting birthday to {}", birthday); + l.set_wallet_initial_state(birthday); l.read_sapling_params(); info!("Created new wallet!"); @@ -310,7 +308,8 @@ impl LightClient { wallet : Arc::new(RwLock::new(wallet)), config : config.clone(), sapling_output : vec![], - sapling_spend : vec![] + sapling_spend : vec![], + sync_lock : Mutex::new(()), }; lc.read_sapling_params(); @@ -727,7 +726,7 @@ impl LightClient { self.wallet.read().unwrap().clear_blocks(); // Then set the initial block - self.set_wallet_initial_state(); + self.set_wallet_initial_state(self.wallet.read().unwrap().get_birthday()); // Then, do a sync, which will force a full rescan from the initial state let response = self.do_sync(true); @@ -737,6 +736,10 @@ impl LightClient { } pub fn do_sync(&self, print_updates: bool) -> String { + // We can only do one sync at a time because we sync blocks in serial order + // If we allow multiple syncs, they'll all get jumbled up. + let _lock = self.sync_lock.lock().unwrap(); + // Sync is 3 parts // 1. Get the latest block // 2. Get all the blocks that we don't have diff --git a/lib/src/lightclient/checkpoints.rs b/lib/src/lightclient/checkpoints.rs new file mode 100644 index 0000000..087c097 --- /dev/null +++ b/lib/src/lightclient/checkpoints.rs @@ -0,0 +1,90 @@ +pub fn get_closest_checkpoint(chain_name: &str, height: u64) -> Option<(u64, &'static str, &'static str)> { + match chain_name { + "test" => get_test_checkpoint(height), + "main" => get_main_checkpoint(height), + _ => None + } +} + +fn get_test_checkpoint(height: u64) -> Option<(u64, &'static str, &'static str)> { + let checkpoints: Vec<(u64, &str, &str)> = vec![ + (105942, "", + "" + ), + (105943, "", + "" + ) + ]; + + find_checkpoint(height, checkpoints) +} + + +fn get_main_checkpoint(height: u64) -> Option<(u64, &'static str, &'static str)> { + let checkpoints: Vec<(u64, &str, &str)> = vec![ + (105942, "00000001c0199f329ee03379bf1387856dbab23765da508bf9b9d8d544f212c0", + "" + ) + ]; + + find_checkpoint(height, checkpoints) +} + +fn find_checkpoint(height: u64, chkpts: Vec<(u64, &'static str, &'static str)>) -> Option<(u64, &'static str, &'static str)> { + // Find the closest checkpoint + let mut heights = chkpts.iter().map(|(h, _, _)| *h as u64).collect::>(); + heights.sort(); + + match get_first_lower_than(height, heights) { + Some(closest_height) => { + chkpts.iter().find(|(h, _, _)| *h == closest_height).map(|t| *t) + }, + None => None + } +} + +fn get_first_lower_than(height: u64, heights: Vec) -> Option { + // If it's before the first checkpoint, return None. + if heights.len() == 0 || height < heights[0] { + return None; + } + + for (i, h) in heights.iter().enumerate() { + if height < *h { + return Some(heights[i-1]); + } + } + + return Some(*heights.last().unwrap()); +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + fn test_lower_than() { + assert_eq!(get_first_lower_than( 9, vec![10, 30, 40]), None); + assert_eq!(get_first_lower_than(10, vec![10, 30, 40]).unwrap(), 10); + assert_eq!(get_first_lower_than(11, vec![10, 30, 40]).unwrap(), 10); + assert_eq!(get_first_lower_than(29, vec![10, 30, 40]).unwrap(), 10); + assert_eq!(get_first_lower_than(30, vec![10, 30, 40]).unwrap(), 30); + assert_eq!(get_first_lower_than(40, vec![10, 30, 40]).unwrap(), 40); + assert_eq!(get_first_lower_than(41, vec![10, 30, 40]).unwrap(), 40); + assert_eq!(get_first_lower_than(99, vec![10, 30, 40]).unwrap(), 40); + } + + #[test] + fn test_checkpoints() { + assert_eq!(get_test_checkpoint(990000), None); + assert_eq!(get_test_checkpoint(100000).unwrap().0, 100000); + assert_eq!(get_test_checkpoint(110000).unwrap().0, 100000); + assert_eq!(get_test_checkpoint(111000).unwrap().0, 1100000); + assert_eq!(get_test_checkpoint(112000).unwrap().0, 1100000); + + assert_eq!(get_main_checkpoint(990000), None); + assert_eq!(get_main_checkpoint(110000).unwrap().0, 110000); + assert_eq!(get_main_checkpoint(111000).unwrap().0, 110000); + } + +} \ No newline at end of file diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index f54eb64..3d799a5 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -360,8 +360,8 @@ impl LightWallet { })?; utils::write_string(&mut writer, &self.config.chain_name)?; - // While writing the birthday, be sure that we're right, and that we don't - // have a tx that is before the current birthday + // While writing the birthday, get it from the fn so we recalculate it properly + // in case of rescans etc... writer.write_u64::(self.get_birthday())?; Ok(()) @@ -375,7 +375,11 @@ impl LightWallet { } pub fn get_birthday(&self) -> u64 { - cmp::min(self.get_first_tx_block(), self.birthday) + if self.birthday == 0 { + self.get_first_tx_block() + } else { + cmp::min(self.get_first_tx_block(), self.birthday) + } } // Get the first block that this wallet has a tx in. This is often used as the wallet's "birthday" @@ -388,7 +392,7 @@ impl LightWallet { .collect::>(); blocks.sort(); - *blocks.first() // Returns optional + *blocks.first() // Returns optional, so if there's no txns, it'll get the activation height .unwrap_or(&cmp::max(self.birthday, self.config.sapling_activation_height)) }