From da2cd163e859532afedf32719ae7e19ae852680d Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Mon, 18 Sep 2023 04:43:32 +0000 Subject: [PATCH] Upgrade to LDK 0.0.117 --- Cargo.toml | 14 ++++---- src/args.rs | 17 +++------- src/cli.rs | 46 ++++++++++++++------------ src/main.rs | 93 ++++++++++++++++++++++++++++++++-------------------- src/sweep.rs | 11 ++++--- 5 files changed, 101 insertions(+), 80 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dbd703e..adac61d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,13 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lightning = { version = "0.0.116", features = ["max_level_trace"] } -lightning-block-sync = { version = "0.0.116", features = [ "rpc-client" ] } -lightning-invoice = { version = "0.24.0" } -lightning-net-tokio = { version = "0.0.116" } -lightning-persister = { version = "0.0.116" } -lightning-background-processor = { version = "0.0.116", features = [ "futures" ] } -lightning-rapid-gossip-sync = { version = "0.0.116" } +lightning = { version = "0.0.117", features = ["max_level_trace"] } +lightning-block-sync = { version = "0.0.117", features = [ "rpc-client" ] } +lightning-invoice = { version = "0.25.0" } +lightning-net-tokio = { version = "0.0.117" } +lightning-persister = { version = "0.0.117" } +lightning-background-processor = { version = "0.0.117", features = [ "futures" ] } +lightning-rapid-gossip-sync = { version = "0.0.117" } base64 = "0.13.0" bitcoin = "0.29.0" diff --git a/src/args.rs b/src/args.rs index b6ae379..3778aa2 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,10 +1,9 @@ use crate::cli::LdkUserInfo; use bitcoin::network::constants::Network; -use lightning::ln::msgs::NetAddress; +use lightning::ln::msgs::SocketAddress; use std::collections::HashMap; use std::env; use std::fs; -use std::net::IpAddr; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -86,18 +85,12 @@ pub(crate) fn parse_startup_args() -> Result { let mut ldk_announced_listen_addr = Vec::new(); loop { match env::args().skip(arg_idx + 1).next().as_ref() { - Some(s) => match IpAddr::from_str(s) { - Ok(IpAddr::V4(a)) => { - ldk_announced_listen_addr - .push(NetAddress::IPv4 { addr: a.octets(), port: ldk_peer_listening_port }); + Some(s) => match SocketAddress::from_str(s) { + Ok(sa) => { + ldk_announced_listen_addr.push(sa); arg_idx += 1; } - Ok(IpAddr::V6(a)) => { - ldk_announced_listen_addr - .push(NetAddress::IPv6 { addr: a.octets(), port: ldk_peer_listening_port }); - arg_idx += 1; - } - Err(_) => panic!("Failed to parse announced-listen-addr into an IP address"), + Err(_) => panic!("Failed to parse announced-listen-addr into a socket address"), }, None => break, } diff --git a/src/cli.rs b/src/cli.rs index 75b7967..121aa98 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,19 +9,19 @@ use bitcoin::hashes::Hash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry}; -use lightning::ln::msgs::NetAddress; -use lightning::ln::{PaymentHash, PaymentPreimage}; +use lightning::ln::msgs::SocketAddress; +use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage}; use lightning::onion_message::OnionMessagePath; use lightning::onion_message::{CustomOnionMessageContents, Destination, OnionMessageContents}; use lightning::routing::gossip::NodeId; use lightning::routing::router::{PaymentParameters, RouteParameters}; use lightning::sign::{EntropySource, KeysManager}; use lightning::util::config::{ChannelHandshakeConfig, ChannelHandshakeLimits, UserConfig}; -use lightning::util::persist::KVStorePersister; +use lightning::util::persist::KVStore; use lightning::util::ser::{Writeable, Writer}; use lightning_invoice::payment::pay_invoice; use lightning_invoice::{utils, Bolt11Invoice, Currency}; -use lightning_persister::FilesystemPersister; +use lightning_persister::fs_store::FilesystemStore; use std::env; use std::io; use std::io::Write; @@ -38,7 +38,7 @@ pub(crate) struct LdkUserInfo { pub(crate) bitcoind_rpc_host: String, pub(crate) ldk_storage_dir_path: String, pub(crate) ldk_peer_listening_port: u16, - pub(crate) ldk_announced_listen_addr: Vec, + pub(crate) ldk_announced_listen_addr: Vec, pub(crate) ldk_announced_node_name: [u8; 32], pub(crate) network: Network, } @@ -65,7 +65,7 @@ pub(crate) fn poll_for_user_input( keys_manager: Arc, network_graph: Arc, onion_messenger: Arc, inbound_payments: Arc>, outbound_payments: Arc>, ldk_data_dir: String, network: Network, - logger: Arc, persister: Arc, + logger: Arc, persister: Arc, ) { println!( "LDK startup successful. Enter \"help\" to view available commands. Press Ctrl-D to quit." @@ -247,7 +247,9 @@ pub(crate) fn poll_for_user_input( expiry_secs.unwrap(), Arc::clone(&logger), ); - persister.persist(INBOUND_PAYMENTS_FNAME, &*inbound_payments).unwrap(); + persister + .write("", "", INBOUND_PAYMENTS_FNAME, &inbound_payments.encode()) + .unwrap(); } "connectpeer" => { let peer_pubkey_and_ip_addr = words.next(); @@ -515,7 +517,7 @@ fn list_channels(channel_manager: &Arc, network_graph: &Arc, network_graph: &Arc, + outbound_payments: &mut PaymentInfoStorage, persister: Arc, ) { let payment_hash = PaymentHash((*invoice.payment_hash()).into_inner()); let payment_secret = Some(*invoice.payment_secret()); @@ -695,7 +697,7 @@ fn send_payment( amt_msat: MillisatAmount(invoice.amount_milli_satoshis()), }, ); - persister.persist(OUTBOUND_PAYMENTS_FNAME, &*outbound_payments).unwrap(); + persister.write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound_payments.encode()).unwrap(); match pay_invoice(invoice, Retry::Timeout(Duration::from_secs(10)), channel_manager) { Ok(_payment_id) => { let payee_pubkey = invoice.recover_payee_pub_key(); @@ -707,22 +709,22 @@ fn send_payment( println!("ERROR: failed to send payment: {:?}", e); print!("> "); outbound_payments.payments.get_mut(&payment_hash).unwrap().status = HTLCStatus::Failed; - persister.persist(OUTBOUND_PAYMENTS_FNAME, &*outbound_payments).unwrap(); + persister.write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound_payments.encode()).unwrap(); } }; } fn keysend( channel_manager: &ChannelManager, payee_pubkey: PublicKey, amt_msat: u64, entropy_source: &E, - outbound_payments: &mut PaymentInfoStorage, persister: Arc, + outbound_payments: &mut PaymentInfoStorage, persister: Arc, ) { let payment_preimage = PaymentPreimage(entropy_source.get_secure_random_bytes()); let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).into_inner()); - let route_params = RouteParameters { - payment_params: PaymentParameters::for_keysend(payee_pubkey, 40, false), - final_value_msat: amt_msat, - }; + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::for_keysend(payee_pubkey, 40, false), + amt_msat, + ); outbound_payments.payments.insert( payment_hash, PaymentInfo { @@ -732,7 +734,7 @@ fn keysend( amt_msat: MillisatAmount(Some(amt_msat)), }, ); - persister.persist(OUTBOUND_PAYMENTS_FNAME, &*outbound_payments).unwrap(); + persister.write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound_payments.encode()).unwrap(); match channel_manager.send_spontaneous_payment_with_retry( Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), @@ -748,7 +750,7 @@ fn keysend( println!("ERROR: failed to send payment: {:?}", e); print!("> "); outbound_payments.payments.get_mut(&payment_hash).unwrap().status = HTLCStatus::Failed; - persister.persist(OUTBOUND_PAYMENTS_FNAME, &*outbound_payments).unwrap(); + persister.write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound_payments.encode()).unwrap(); } }; } @@ -799,7 +801,7 @@ fn get_invoice( fn close_channel( channel_id: [u8; 32], counterparty_node_id: PublicKey, channel_manager: Arc, ) { - match channel_manager.close_channel(&channel_id, &counterparty_node_id) { + match channel_manager.close_channel(&ChannelId(channel_id), &counterparty_node_id) { Ok(()) => println!("EVENT: initiating channel close"), Err(e) => println!("ERROR: failed to close channel: {:?}", e), } @@ -808,7 +810,9 @@ fn close_channel( fn force_close_channel( channel_id: [u8; 32], counterparty_node_id: PublicKey, channel_manager: Arc, ) { - match channel_manager.force_close_broadcasting_latest_txn(&channel_id, &counterparty_node_id) { + match channel_manager + .force_close_broadcasting_latest_txn(&ChannelId(channel_id), &counterparty_node_id) + { Ok(()) => println!("EVENT: initiating channel force-close"), Err(e) => println!("ERROR: failed to force-close channel: {:?}", e), } diff --git a/src/main.rs b/src/main.rs index 40240a7..69f0981 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ use lightning::ln::channelmanager::{ }; use lightning::ln::msgs::DecodeError; use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler, SimpleArcPeerManager}; -use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; +use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; use lightning::onion_message::{DefaultMessageRouter, SimpleArcOnionMessenger}; use lightning::routing::gossip; use lightning::routing::gossip::{NodeId, P2PGossipSync}; @@ -32,7 +32,7 @@ use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::ProbabilisticScoringFeeParameters; use lightning::sign::{EntropySource, InMemorySigner, KeysManager, SpendableOutputDescriptor}; use lightning::util::config::UserConfig; -use lightning::util::persist::KVStorePersister; +use lightning::util::persist::{self, read_channel_monitors, KVStore}; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use lightning::{chain, impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; use lightning_background_processor::{process_events_async, GossipSync}; @@ -41,7 +41,7 @@ use lightning_block_sync::poll; use lightning_block_sync::SpvClient; use lightning_block_sync::UnboundedCache; use lightning_net_tokio::SocketDescriptor; -use lightning_persister::FilesystemPersister; +use lightning_persister::fs_store::FilesystemStore; use rand::{thread_rng, Rng}; use std::collections::hash_map::Entry; use std::collections::HashMap; @@ -53,7 +53,7 @@ use std::io; use std::io::Write; use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, SystemTime}; pub(crate) const PENDING_SPENDABLE_OUTPUT_DIR: &'static str = "pending_spendable_outputs"; @@ -123,7 +123,7 @@ type ChainMonitor = chainmonitor::ChainMonitor< Arc, Arc, Arc, - Arc, + Arc, >; pub(crate) type PeerManager = SimpleArcPeerManager< @@ -131,7 +131,7 @@ pub(crate) type PeerManager = SimpleArcPeerManager< ChainMonitor, BitcoindClient, BitcoindClient, - BitcoindClient, + Arc, FilesystemLogger, >; @@ -153,7 +153,7 @@ async fn handle_ldk_events( channel_manager: &Arc, bitcoind_client: &BitcoindClient, network_graph: &NetworkGraph, keys_manager: &KeysManager, bump_tx_event_handler: &BumpTxEventHandler, inbound_payments: Arc>, - outbound_payments: Arc>, persister: &Arc, + outbound_payments: Arc>, persister: &Arc, network: Network, event: Event, ) { match event { @@ -229,7 +229,14 @@ async fn handle_ldk_events( }; channel_manager.claim_funds(payment_preimage.unwrap()); } - Event::PaymentClaimed { payment_hash, purpose, amount_msat, receiver_node_id: _ } => { + Event::PaymentClaimed { + payment_hash, + purpose, + amount_msat, + receiver_node_id: _, + htlcs: _, + sender_intended_total_msat: _, + } => { println!( "\nEVENT: claimed payment from payment hash {} of {} millisatoshis", hex_utils::hex_str(&payment_hash.0), @@ -260,7 +267,7 @@ async fn handle_ldk_events( }); } } - persister.persist(INBOUND_PAYMENTS_FNAME, &*inbound).unwrap(); + persister.write("", "", INBOUND_PAYMENTS_FNAME, &inbound.encode()).unwrap(); } Event::PaymentSent { payment_preimage, payment_hash, fee_paid_msat, .. } => { let mut outbound = outbound_payments.lock().unwrap(); @@ -284,7 +291,7 @@ async fn handle_ldk_events( io::stdout().flush().unwrap(); } } - persister.persist(OUTBOUND_PAYMENTS_FNAME, &*outbound).unwrap(); + persister.write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound.encode()).unwrap(); } Event::OpenChannelRequest { ref temporary_channel_id, ref counterparty_node_id, .. @@ -301,14 +308,14 @@ async fn handle_ldk_events( if let Err(e) = res { print!( "\nEVENT: Failed to accept inbound channel ({}) from {}: {:?}", - hex_utils::hex_str(&temporary_channel_id[..]), + temporary_channel_id, hex_utils::hex_str(&counterparty_node_id.serialize()), e, ); } else { print!( "\nEVENT: Accepted inbound channel ({}) from {}", - hex_utils::hex_str(&temporary_channel_id[..]), + temporary_channel_id, hex_utils::hex_str(&counterparty_node_id.serialize()), ); } @@ -333,7 +340,7 @@ async fn handle_ldk_events( let payment = outbound.payments.get_mut(&payment_hash).unwrap(); payment.status = HTLCStatus::Failed; } - persister.persist(OUTBOUND_PAYMENTS_FNAME, &*outbound).unwrap(); + persister.write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound.encode()).unwrap(); } Event::PaymentForwarded { prev_channel_id, @@ -346,7 +353,7 @@ async fn handle_ldk_events( let nodes = read_only_network_graph.nodes(); let channels = channel_manager.list_channels(); - let node_str = |channel_id: &Option<[u8; 32]>| match channel_id { + let node_str = |channel_id: &Option| match channel_id { None => String::new(), Some(channel_id) => match channels.iter().find(|c| c.channel_id == *channel_id) { None => String::new(), @@ -363,9 +370,9 @@ async fn handle_ldk_events( } }, }; - let channel_str = |channel_id: &Option<[u8; 32]>| { + let channel_str = |channel_id: &Option| { channel_id - .map(|channel_id| format!(" with channel {}", hex_utils::hex_str(&channel_id))) + .map(|channel_id| format!(" with channel {}", channel_id)) .unwrap_or_default() }; let from_prev_str = @@ -407,7 +414,7 @@ async fn handle_ldk_events( forwarding_channel_manager.process_pending_htlc_forwards(); }); } - Event::SpendableOutputs { outputs } => { + Event::SpendableOutputs { outputs, channel_id: _ } => { // SpendableOutputDescriptors, of which outputs is a vec of, are critical to keep track // of! While a `StaticOutput` descriptor is just an output to a static, well-known key, // other descriptors are not currently ever regenerated for you by LDK. Once we return @@ -421,15 +428,13 @@ async fn handle_ldk_events( let key = hex_utils::hex_str(&keys_manager.get_secure_random_bytes()); // Note that if the type here changes our read code needs to change as well. let output: SpendableOutputDescriptor = output; - persister - .persist(&format!("{}/{}", PENDING_SPENDABLE_OUTPUT_DIR, key), &output) - .unwrap(); + persister.write(PENDING_SPENDABLE_OUTPUT_DIR, "", &key, &output.encode()).unwrap(); } } Event::ChannelPending { channel_id, counterparty_node_id, .. } => { println!( "\nEVENT: Channel {} with peer {} is pending awaiting funding lock-in!", - hex_utils::hex_str(&channel_id), + channel_id, hex_utils::hex_str(&counterparty_node_id.serialize()), ); print!("> "); @@ -443,16 +448,23 @@ async fn handle_ldk_events( } => { println!( "\nEVENT: Channel {} with peer {} is ready to be used!", - hex_utils::hex_str(channel_id), + channel_id, hex_utils::hex_str(&counterparty_node_id.serialize()), ); print!("> "); io::stdout().flush().unwrap(); } - Event::ChannelClosed { channel_id, reason, user_channel_id: _ } => { + Event::ChannelClosed { + channel_id, + reason, + user_channel_id: _, + counterparty_node_id, + channel_capacity_sats: _, + } => { println!( - "\nEVENT: Channel {} closed due to: {:?}", - hex_utils::hex_str(&channel_id), + "\nEVENT: Channel {} with counterparty {} closed due to: {:?}", + channel_id, + counterparty_node_id.map(|id| format!("{}", id)).unwrap_or("".to_owned()), reason ); print!("> "); @@ -527,7 +539,7 @@ async fn start_ldk() { let broadcaster = bitcoind_client.clone(); // Step 4: Initialize Persist - let persister = Arc::new(FilesystemPersister::new(ldk_data_dir.clone())); + let persister = Arc::new(FilesystemStore::new(ldk_data_dir.clone().into())); // Step 5: Initialize the ChainMonitor let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( @@ -575,7 +587,8 @@ async fn start_ldk() { // Step 7: Read ChannelMonitor state from disk let mut channelmonitors = - persister.read_channelmonitors(keys_manager.clone(), keys_manager.clone()).unwrap(); + read_channel_monitors(Arc::clone(&persister), keys_manager.clone(), keys_manager.clone()) + .unwrap(); // Step 8: Poll for the best chain tip, which may be used by the channel manager & spv client let polled_chain_tip = init::validate_best_block_header(bitcoind_client.as_ref()) @@ -588,7 +601,7 @@ async fn start_ldk() { Arc::new(disk::read_network(Path::new(&network_graph_path), args.network, logger.clone())); let scorer_path = format!("{}/scorer", ldk_data_dir.clone()); - let scorer = Arc::new(Mutex::new(disk::read_scorer( + let scorer = Arc::new(RwLock::new(disk::read_scorer( Path::new(&scorer_path), Arc::clone(&network_graph), Arc::clone(&logger), @@ -697,7 +710,7 @@ async fn start_ldk() { let funding_outpoint = item.2; assert_eq!( chain_monitor.watch_channel(funding_outpoint, channel_monitor), - ChannelMonitorUpdateStatus::Completed + Ok(ChannelMonitorUpdateStatus::Completed) ); } @@ -790,8 +803,9 @@ async fn start_ldk() { .into_iter() .filter_map(|p| match p { RecentPaymentDetails::Pending { payment_hash, .. } => Some(payment_hash), - RecentPaymentDetails::Fulfilled { payment_hash } => payment_hash, - RecentPaymentDetails::Abandoned { payment_hash } => Some(payment_hash), + RecentPaymentDetails::Fulfilled { payment_hash, .. } => payment_hash, + RecentPaymentDetails::Abandoned { payment_hash, .. } => Some(payment_hash), + RecentPaymentDetails::AwaitingInvoice { payment_id: _ } => todo!(), }) .collect::>(); for (payment_hash, payment_info) in outbound_payments @@ -805,7 +819,9 @@ async fn start_ldk() { payment_info.status = HTLCStatus::Failed; } } - persister.persist(OUTBOUND_PAYMENTS_FNAME, &*outbound_payments.lock().unwrap()).unwrap(); + persister + .write("", "", OUTBOUND_PAYMENTS_FNAME, &outbound_payments.lock().unwrap().encode()) + .unwrap(); // Step 18: Handle LDK Events let channel_manager_event_listener = Arc::clone(&channel_manager); @@ -843,7 +859,7 @@ async fn start_ldk() { }; // Step 19: Persist ChannelManager and NetworkGraph - let persister = Arc::new(FilesystemPersister::new(ldk_data_dir.clone())); + let persister = Arc::new(FilesystemStore::new(ldk_data_dir.clone().into())); // Step 20: Background Processing let (bp_exit, bp_exit_check) = tokio::sync::watch::channel(()); @@ -871,7 +887,7 @@ async fn start_ldk() { // Regularly reconnect to channel peers. let connect_cm = Arc::clone(&channel_manager); let connect_pm = Arc::clone(&peer_manager); - let peer_data_path = format!("{}/channel_peer_data", ldk_data_dir.clone()); + let peer_data_path = format!("{}/channel_peer_data", ldk_data_dir); let stop_connect = Arc::clone(&stop_listen_connect); tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(1)); @@ -979,7 +995,14 @@ async fn start_ldk() { peer_manager.disconnect_all_peers(); if let Err(e) = bg_res { - let persist_res = persister.persist("manager", &*channel_manager).unwrap(); + let persist_res = persister + .write( + persist::CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + persist::CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, + persist::CHANNEL_MANAGER_PERSISTENCE_KEY, + &channel_manager.encode(), + ) + .unwrap(); use lightning::util::logger::Logger; lightning::log_error!( &*logger, diff --git a/src/sweep.rs b/src/sweep.rs index 94120f9..01cea9d 100644 --- a/src/sweep.rs +++ b/src/sweep.rs @@ -7,8 +7,10 @@ use std::{fs, io}; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; use lightning::sign::{EntropySource, KeysManager, SpendableOutputDescriptor}; use lightning::util::logger::Logger; -use lightning::util::persist::KVStorePersister; -use lightning::util::ser::{Readable, WithoutLength}; +use lightning::util::persist::KVStore; +use lightning::util::ser::{Readable, WithoutLength, Writeable}; + +use lightning_persister::fs_store::FilesystemStore; use bitcoin::secp256k1::Secp256k1; use bitcoin::{LockTime, PackedLockTime}; @@ -18,7 +20,6 @@ use crate::hex_utils; use crate::BitcoindClient; use crate::ChannelManager; use crate::FilesystemLogger; -use crate::FilesystemPersister; /// If we have any pending claimable outputs, we should slowly sweep them to our Bitcoin Core /// wallet. We technically don't need to do this - they're ours to spend when we want and can just @@ -30,7 +31,7 @@ use crate::FilesystemPersister; /// we don't do that here either. pub(crate) async fn periodic_sweep( ldk_data_dir: String, keys_manager: Arc, logger: Arc, - persister: Arc, bitcoind_client: Arc, + persister: Arc, bitcoind_client: Arc, channel_manager: Arc, ) { // Regularly claim outputs which are exclusively spendable by us and send them to Bitcoin Core. @@ -79,7 +80,7 @@ pub(crate) async fn periodic_sweep( if !outputs.is_empty() { let key = hex_utils::hex_str(&keys_manager.get_secure_random_bytes()); persister - .persist(&format!("spendable_outputs/{}", key), &WithoutLength(&outputs)) + .write("spendable_outputs", "", &key, &WithoutLength(&outputs).encode()) .unwrap(); fs::remove_dir_all(&processing_spendables_dir).unwrap(); } -- 2.30.2