No flags found
Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.
e.g., #unittest #integration
#production #enterprise
#frontend #backend
Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.
e.g., #unittest #integration
#production #enterprise
#frontend #backend
3693 | 3693 | } |
|
3694 | 3694 | } |
|
3695 | 3695 | ||
3696 | - | impl<ChanSigner: ChannelKeys + Writeable, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> Writeable for ChannelManager<ChanSigner, M, T, K, F, L> |
|
3696 | + | impl<ChanSigner: ChannelKeys, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> Writeable for ChannelManager<ChanSigner, M, T, K, F, L> |
|
3697 | 3697 | where M::Target: chain::Watch<Keys=ChanSigner>, |
|
3698 | 3698 | T::Target: BroadcasterInterface, |
|
3699 | 3699 | K::Target: KeysInterface<ChanKeySigner = ChanSigner>, |
3784 | 3784 | L::Target: Logger, |
|
3785 | 3785 | { |
|
3786 | 3786 | /// The keys provider which will give us relevant keys. Some keys will be loaded during |
|
3787 | - | /// deserialization. |
|
3787 | + | /// deserialization and KeysInterface::read_chan_signer will be used to read per-Channel |
|
3788 | + | /// signing data. |
|
3788 | 3789 | pub keys_manager: K, |
|
3789 | 3790 | ||
3790 | 3791 | /// The fee_estimator for use in the ChannelManager in the future. |
3846 | 3847 | ||
3847 | 3848 | // Implement ReadableArgs for an Arc'd ChannelManager to make it a bit easier to work with the |
|
3848 | 3849 | // SipmleArcChannelManager type: |
|
3849 | - | impl<'a, ChanSigner: ChannelKeys + Readable, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> |
|
3850 | + | impl<'a, ChanSigner: ChannelKeys, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> |
|
3850 | 3851 | ReadableArgs<ChannelManagerReadArgs<'a, ChanSigner, M, T, K, F, L>> for (BlockHash, Arc<ChannelManager<ChanSigner, M, T, K, F, L>>) |
|
3851 | 3852 | where M::Target: chain::Watch<Keys=ChanSigner>, |
|
3852 | 3853 | T::Target: BroadcasterInterface, |
3860 | 3861 | } |
|
3861 | 3862 | } |
|
3862 | 3863 | ||
3863 | - | impl<'a, ChanSigner: ChannelKeys + Readable, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> |
|
3864 | + | impl<'a, ChanSigner: ChannelKeys, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> |
|
3864 | 3865 | ReadableArgs<ChannelManagerReadArgs<'a, ChanSigner, M, T, K, F, L>> for (BlockHash, ChannelManager<ChanSigner, M, T, K, F, L>) |
|
3865 | 3866 | where M::Target: chain::Watch<Keys=ChanSigner>, |
|
3866 | 3867 | T::Target: BroadcasterInterface, |
3886 | 3887 | let mut by_id = HashMap::with_capacity(cmp::min(channel_count as usize, 128)); |
|
3887 | 3888 | let mut short_to_id = HashMap::with_capacity(cmp::min(channel_count as usize, 128)); |
|
3888 | 3889 | for _ in 0..channel_count { |
|
3889 | - | let mut channel: Channel<ChanSigner> = Readable::read(reader)?; |
|
3890 | + | let mut channel: Channel<ChanSigner> = Channel::read(reader, &args.keys_manager)?; |
|
3890 | 3891 | if channel.last_block_connected != Default::default() && channel.last_block_connected != last_block_hash { |
|
3891 | 3892 | return Err(DecodeError::InvalidValue); |
|
3892 | 3893 | } |
29 | 29 | ||
30 | 30 | extern crate bitcoin; |
|
31 | 31 | #[cfg(any(test, feature = "_test_utils"))] extern crate hex; |
|
32 | - | #[cfg(any(test, feature = "_test_utils"))] extern crate regex; |
|
32 | + | #[cfg(any(test, feature = "fuzztarget", feature = "_test_utils"))] extern crate regex; |
|
33 | 33 | ||
34 | 34 | #[macro_use] |
|
35 | 35 | pub mod util; |
26 | 26 | use util::enforcing_trait_impls::EnforcingChannelKeys; |
|
27 | 27 | use util::events::{Event, EventsProvider, MessageSendEvent, MessageSendEventsProvider}; |
|
28 | 28 | use util::errors::APIError; |
|
29 | - | use util::ser::Readable; |
|
29 | + | use util::ser::{ReadableArgs, Writeable}; |
|
30 | 30 | ||
31 | 31 | use bitcoin::hashes::sha256::Hash as Sha256; |
|
32 | 32 | use bitcoin::hashes::Hash; |
105 | 105 | let monitors = nodes[0].chain_monitor.chain_monitor.monitors.lock().unwrap(); |
|
106 | 106 | let monitor = monitors.get(&outpoint).unwrap(); |
|
107 | 107 | let mut w = test_utils::TestVecWriter(Vec::new()); |
|
108 | - | monitor.serialize_for_disk(&mut w).unwrap(); |
|
108 | + | monitor.write(&mut w).unwrap(); |
|
109 | 109 | let new_monitor = <(BlockHash, ChannelMonitor<EnforcingChannelKeys>)>::read( |
|
110 | - | &mut ::std::io::Cursor::new(&w.0)).unwrap().1; |
|
110 | + | &mut ::std::io::Cursor::new(&w.0), &test_utils::OnlyReadsKeysInterface {}).unwrap().1; |
|
111 | 111 | assert!(new_monitor == *monitor); |
|
112 | 112 | let chain_mon = test_utils::TestChainMonitor::new(Some(&chain_source), &chanmon_cfgs[0].tx_broadcaster, &logger, &chanmon_cfgs[0].fee_estimator, &persister); |
|
113 | 113 | assert!(chain_mon.watch_channel(outpoint, new_monitor).is_ok()); |
43 | 43 | use ln::onchaintx::{OnchainTxHandler, InputDescriptors}; |
|
44 | 44 | use chain::chaininterface::{BroadcasterInterface, FeeEstimator}; |
|
45 | 45 | use chain::transaction::{OutPoint, TransactionData}; |
|
46 | - | use chain::keysinterface::{SpendableOutputDescriptor, ChannelKeys}; |
|
46 | + | use chain::keysinterface::{SpendableOutputDescriptor, ChannelKeys, KeysInterface}; |
|
47 | 47 | use util::logger::Logger; |
|
48 | - | use util::ser::{Readable, MaybeReadable, Writer, Writeable, U48}; |
|
48 | + | use util::ser::{Readable, ReadableArgs, MaybeReadable, Writer, Writeable, U48}; |
|
49 | 49 | use util::byte_utils; |
|
50 | 50 | use util::events::Event; |
|
51 | 51 |
56 | 56 | ||
57 | 57 | /// An update generated by the underlying Channel itself which contains some new information the |
|
58 | 58 | /// ChannelMonitor should be made aware of. |
|
59 | - | #[cfg_attr(any(test, feature = "_test_utils"), derive(PartialEq))] |
|
59 | + | #[cfg_attr(any(test, feature = "fuzztarget", feature = "_test_utils"), derive(PartialEq))] |
|
60 | 60 | #[derive(Clone)] |
|
61 | 61 | #[must_use] |
|
62 | 62 | pub struct ChannelMonitorUpdate { |
485 | 485 | const SERIALIZATION_VERSION: u8 = 1; |
|
486 | 486 | const MIN_SERIALIZATION_VERSION: u8 = 1; |
|
487 | 487 | ||
488 | - | #[cfg_attr(any(test, feature = "_test_utils"), derive(PartialEq))] |
|
488 | + | #[cfg_attr(any(test, feature = "fuzztarget", feature = "_test_utils"), derive(PartialEq))] |
|
489 | 489 | #[derive(Clone)] |
|
490 | 490 | pub(crate) enum ChannelMonitorUpdateStep { |
|
491 | 491 | LatestHolderCommitmentTXInfo { |
617 | 617 | /// get_and_clear_pending_monitor_events or get_and_clear_pending_events are serialized to disk and |
|
618 | 618 | /// reloaded at deserialize-time. Thus, you must ensure that, when handling events, all events |
|
619 | 619 | /// gotten are fully handled before re-serializing the new state. |
|
620 | + | /// |
|
621 | + | /// Note that the deserializer is only implemented for (Sha256dHash, ChannelMonitor), which |
|
622 | + | /// tells you the last block hash which was block_connect()ed. You MUST rescan any blocks along |
|
623 | + | /// the "reorg path" (ie disconnecting blocks until you find a common ancestor from both the |
|
624 | + | /// returned block hash and the the current chain and then reconnecting blocks to get to the |
|
625 | + | /// best chain) upon deserializing the object! |
|
620 | 626 | pub struct ChannelMonitor<ChanSigner: ChannelKeys> { |
|
621 | 627 | latest_update_id: u64, |
|
622 | 628 | commitment_transaction_number_obscure_factor: u64, |
626 | 632 | counterparty_payment_script: Script, |
|
627 | 633 | shutdown_script: Script, |
|
628 | 634 | ||
629 | - | keys: ChanSigner, |
|
635 | + | key_derivation_params: (u64, u64), |
|
636 | + | holder_revocation_basepoint: PublicKey, |
|
630 | 637 | funding_info: (OutPoint, Script), |
|
631 | 638 | current_counterparty_commitment_txid: Option<Txid>, |
|
632 | 639 | prev_counterparty_commitment_txid: Option<Txid>, |
721 | 728 | self.destination_script != other.destination_script || |
|
722 | 729 | self.broadcasted_holder_revokable_script != other.broadcasted_holder_revokable_script || |
|
723 | 730 | self.counterparty_payment_script != other.counterparty_payment_script || |
|
724 | - | self.keys.pubkeys() != other.keys.pubkeys() || |
|
731 | + | self.key_derivation_params != other.key_derivation_params || |
|
732 | + | self.holder_revocation_basepoint != other.holder_revocation_basepoint || |
|
725 | 733 | self.funding_info != other.funding_info || |
|
726 | 734 | self.current_counterparty_commitment_txid != other.current_counterparty_commitment_txid || |
|
727 | 735 | self.prev_counterparty_commitment_txid != other.prev_counterparty_commitment_txid || |
753 | 761 | } |
|
754 | 762 | } |
|
755 | 763 | ||
756 | - | impl<ChanSigner: ChannelKeys + Writeable> ChannelMonitor<ChanSigner> { |
|
757 | - | /// Writes this monitor into the given writer, suitable for writing to disk. |
|
758 | - | /// |
|
759 | - | /// Note that the deserializer is only implemented for (Sha256dHash, ChannelMonitor), which |
|
760 | - | /// tells you the last block hash which was block_connect()ed. You MUST rescan any blocks along |
|
761 | - | /// the "reorg path" (ie disconnecting blocks until you find a common ancestor from both the |
|
762 | - | /// returned block hash and the the current chain and then reconnecting blocks to get to the |
|
763 | - | /// best chain) upon deserializing the object! |
|
764 | - | pub fn serialize_for_disk<W: Writer>(&self, writer: &mut W) -> Result<(), Error> { |
|
764 | + | impl<ChanSigner: ChannelKeys> Writeable for ChannelMonitor<ChanSigner> { |
|
765 | + | fn write<W: Writer>(&self, writer: &mut W) -> Result<(), Error> { |
|
765 | 766 | //TODO: We still write out all the serialization here manually instead of using the fancy |
|
766 | 767 | //serialization framework we have, we should migrate things over to it. |
|
767 | 768 | writer.write_all(&[SERIALIZATION_VERSION; 1])?; |
785 | 786 | self.counterparty_payment_script.write(writer)?; |
|
786 | 787 | self.shutdown_script.write(writer)?; |
|
787 | 788 | ||
788 | - | self.keys.write(writer)?; |
|
789 | + | self.key_derivation_params.write(writer)?; |
|
790 | + | self.holder_revocation_basepoint.write(writer)?; |
|
789 | 791 | writer.write_all(&self.funding_info.0.txid[..])?; |
|
790 | 792 | writer.write_all(&byte_utils::be16_to_array(self.funding_info.0.index))?; |
|
791 | 793 | self.funding_info.1.write(writer)?; |
965 | 967 | let counterparty_htlc_base_key = counterparty_channel_parameters.pubkeys.htlc_basepoint; |
|
966 | 968 | let counterparty_tx_cache = CounterpartyCommitmentTransaction { counterparty_delayed_payment_base_key, counterparty_htlc_base_key, on_counterparty_tx_csv, per_htlc: HashMap::new() }; |
|
967 | 969 | ||
968 | - | let mut onchain_tx_handler = OnchainTxHandler::new(destination_script.clone(), keys.clone(), channel_parameters.clone()); |
|
970 | + | let key_derivation_params = keys.key_derivation_params(); |
|
971 | + | let holder_revocation_basepoint = keys.pubkeys().revocation_basepoint; |
|
972 | + | let mut onchain_tx_handler = OnchainTxHandler::new(destination_script.clone(), keys, channel_parameters.clone()); |
|
969 | 973 | ||
970 | 974 | let secp_ctx = Secp256k1::new(); |
|
971 | 975 |
1001 | 1005 | counterparty_payment_script, |
|
1002 | 1006 | shutdown_script, |
|
1003 | 1007 | ||
1004 | - | keys, |
|
1008 | + | key_derivation_params, |
|
1009 | + | holder_revocation_basepoint, |
|
1005 | 1010 | funding_info, |
|
1006 | 1011 | current_counterparty_commitment_txid: None, |
|
1007 | 1012 | prev_counterparty_commitment_txid: None, |
1373 | 1378 | let secret = self.get_secret(commitment_number).unwrap(); |
|
1374 | 1379 | let per_commitment_key = ignore_error!(SecretKey::from_slice(&secret)); |
|
1375 | 1380 | let per_commitment_point = PublicKey::from_secret_key(&self.secp_ctx, &per_commitment_key); |
|
1376 | - | let revocation_pubkey = ignore_error!(chan_utils::derive_public_revocation_key(&self.secp_ctx, &per_commitment_point, &self.keys.pubkeys().revocation_basepoint)); |
|
1381 | + | let revocation_pubkey = ignore_error!(chan_utils::derive_public_revocation_key(&self.secp_ctx, &per_commitment_point, &self.holder_revocation_basepoint)); |
|
1377 | 1382 | let delayed_key = ignore_error!(chan_utils::derive_public_key(&self.secp_ctx, &PublicKey::from_secret_key(&self.secp_ctx, &per_commitment_key), &self.counterparty_tx_cache.counterparty_delayed_payment_base_key)); |
|
1378 | 1383 | ||
1379 | 1384 | let revokeable_redeemscript = chan_utils::get_revokeable_redeemscript(&revocation_pubkey, self.counterparty_tx_cache.on_counterparty_tx_csv, &delayed_key); |
2205 | 2210 | per_commitment_point: broadcasted_holder_revokable_script.1, |
|
2206 | 2211 | to_self_delay: self.on_holder_tx_csv, |
|
2207 | 2212 | output: outp.clone(), |
|
2208 | - | key_derivation_params: self.keys.key_derivation_params(), |
|
2213 | + | key_derivation_params: self.key_derivation_params, |
|
2209 | 2214 | revocation_pubkey: broadcasted_holder_revokable_script.2.clone(), |
|
2210 | 2215 | }); |
|
2211 | 2216 | break; |
2214 | 2219 | spendable_output = Some(SpendableOutputDescriptor::StaticOutputCounterpartyPayment { |
|
2215 | 2220 | outpoint: OutPoint { txid: tx.txid(), index: i as u16 }, |
|
2216 | 2221 | output: outp.clone(), |
|
2217 | - | key_derivation_params: self.keys.key_derivation_params(), |
|
2222 | + | key_derivation_params: self.key_derivation_params, |
|
2218 | 2223 | }); |
|
2219 | 2224 | break; |
|
2220 | 2225 | } else if outp.script_pubkey == self.shutdown_script { |
2296 | 2301 | ||
2297 | 2302 | const MAX_ALLOC_SIZE: usize = 64*1024; |
|
2298 | 2303 | ||
2299 | - | impl<ChanSigner: ChannelKeys + Readable> Readable for (BlockHash, ChannelMonitor<ChanSigner>) { |
|
2300 | - | fn read<R: ::std::io::Read>(reader: &mut R) -> Result<Self, DecodeError> { |
|
2304 | + | impl<'a, ChanSigner: ChannelKeys, K: KeysInterface<ChanKeySigner = ChanSigner>> ReadableArgs<&'a K> |
|
2305 | + | for (BlockHash, ChannelMonitor<ChanSigner>) { |
|
2306 | + | fn read<R: ::std::io::Read>(reader: &mut R, keys_manager: &'a K) -> Result<Self, DecodeError> { |
|
2301 | 2307 | macro_rules! unwrap_obj { |
|
2302 | 2308 | ($key: expr) => { |
|
2303 | 2309 | match $key { |
2330 | 2336 | let counterparty_payment_script = Readable::read(reader)?; |
|
2331 | 2337 | let shutdown_script = Readable::read(reader)?; |
|
2332 | 2338 | ||
2333 | - | let keys = Readable::read(reader)?; |
|
2339 | + | let key_derivation_params = Readable::read(reader)?; |
|
2340 | + | let holder_revocation_basepoint = Readable::read(reader)?; |
|
2334 | 2341 | // Technically this can fail and serialize fail a round-trip, but only for serialization of |
|
2335 | 2342 | // barely-init'd ChannelMonitors that we can't do anything with. |
|
2336 | 2343 | let outpoint = OutPoint { |
2530 | 2537 | return Err(DecodeError::InvalidValue); |
|
2531 | 2538 | } |
|
2532 | 2539 | } |
|
2533 | - | let onchain_tx_handler = Readable::read(reader)?; |
|
2540 | + | let onchain_tx_handler = ReadableArgs::read(reader, keys_manager)?; |
|
2534 | 2541 | ||
2535 | 2542 | let lockdown_from_offchain = Readable::read(reader)?; |
|
2536 | 2543 | let holder_tx_signed = Readable::read(reader)?; |
2544 | 2551 | counterparty_payment_script, |
|
2545 | 2552 | shutdown_script, |
|
2546 | 2553 | ||
2547 | - | keys, |
|
2554 | + | key_derivation_params, |
|
2555 | + | holder_revocation_basepoint, |
|
2548 | 2556 | funding_info, |
|
2549 | 2557 | current_counterparty_commitment_txid, |
|
2550 | 2558 | prev_counterparty_commitment_txid, |
33 | 33 | use chain::transaction::{OutPoint, TransactionData}; |
|
34 | 34 | use chain::keysinterface::{ChannelKeys, KeysInterface}; |
|
35 | 35 | use util::transaction_utils; |
|
36 | - | use util::ser::{Readable, Writeable, Writer}; |
|
36 | + | use util::ser::{Readable, ReadableArgs, Writeable, Writer, VecWriter}; |
|
37 | 37 | use util::logger::Logger; |
|
38 | 38 | use util::errors::APIError; |
|
39 | 39 | use util::config::{UserConfig,ChannelConfig}; |
4055 | 4055 | } |
|
4056 | 4056 | } |
|
4057 | 4057 | ||
4058 | - | impl<ChanSigner: ChannelKeys + Writeable> Writeable for Channel<ChanSigner> { |
|
4058 | + | impl<ChanSigner: ChannelKeys> Writeable for Channel<ChanSigner> { |
|
4059 | 4059 | fn write<W: Writer>(&self, writer: &mut W) -> Result<(), ::std::io::Error> { |
|
4060 | 4060 | // Note that we write out as if remove_uncommitted_htlcs_and_mark_paused had just been |
|
4061 | 4061 | // called but include holding cell updates (and obviously we don't modify self). |
4072 | 4072 | ||
4073 | 4073 | self.latest_monitor_update_id.write(writer)?; |
|
4074 | 4074 | ||
4075 | - | self.holder_keys.write(writer)?; |
|
4075 | + | let mut key_data = VecWriter(Vec::new()); |
|
4076 | + | self.holder_keys.write(&mut key_data)?; |
|
4077 | + | assert!(key_data.0.len() < std::usize::MAX); |
|
4078 | + | assert!(key_data.0.len() < std::u32::MAX as usize); |
|
4079 | + | (key_data.0.len() as u32).write(writer)?; |
|
4080 | + | writer.write_all(&key_data.0[..])?; |
|
4081 | + | ||
4076 | 4082 | self.shutdown_pubkey.write(writer)?; |
|
4077 | 4083 | self.destination_script.write(writer)?; |
|
4078 | 4084 |
4237 | 4243 | } |
|
4238 | 4244 | } |
|
4239 | 4245 | ||
4240 | - | impl<ChanSigner: ChannelKeys + Readable> Readable for Channel<ChanSigner> { |
|
4241 | - | fn read<R : ::std::io::Read>(reader: &mut R) -> Result<Self, DecodeError> { |
|
4246 | + | const MAX_ALLOC_SIZE: usize = 64*1024; |
|
4247 | + | impl<'a, ChanSigner: ChannelKeys, K: Deref> ReadableArgs<&'a K> for Channel<ChanSigner> |
|
4248 | + | where K::Target: KeysInterface<ChanKeySigner = ChanSigner> { |
|
4249 | + | fn read<R : ::std::io::Read>(reader: &mut R, keys_source: &'a K) -> Result<Self, DecodeError> { |
|
4242 | 4250 | let _ver: u8 = Readable::read(reader)?; |
|
4243 | 4251 | let min_ver: u8 = Readable::read(reader)?; |
|
4244 | 4252 | if min_ver > SERIALIZATION_VERSION { |
4254 | 4262 | ||
4255 | 4263 | let latest_monitor_update_id = Readable::read(reader)?; |
|
4256 | 4264 | ||
4257 | - | let holder_keys = Readable::read(reader)?; |
|
4265 | + | let keys_len: u32 = Readable::read(reader)?; |
|
4266 | + | let mut keys_data = Vec::with_capacity(cmp::min(keys_len as usize, MAX_ALLOC_SIZE)); |
|
4267 | + | while keys_data.len() != keys_len as usize { |
|
4268 | + | // Read 1KB at a time to avoid accidentally allocating 4GB on corrupted channel keys |
|
4269 | + | let mut data = [0; 1024]; |
|
4270 | + | let read_slice = &mut data[0..cmp::min(1024, keys_len as usize - keys_data.len())]; |
|
4271 | + | reader.read_exact(read_slice)?; |
|
4272 | + | keys_data.extend_from_slice(read_slice); |
|
4273 | + | } |
|
4274 | + | let holder_keys = keys_source.read_chan_signer(&keys_data)?; |
|
4275 | + | ||
4258 | 4276 | let shutdown_pubkey = Readable::read(reader)?; |
|
4259 | 4277 | let destination_script = Readable::read(reader)?; |
|
4260 | 4278 |
4472 | 4490 | use ln::channel::{Channel,ChannelKeys,InboundHTLCOutput,OutboundHTLCOutput,InboundHTLCState,OutboundHTLCState,HTLCOutputInCommitment,TxCreationKeys}; |
|
4473 | 4491 | use ln::channel::MAX_FUNDING_SATOSHIS; |
|
4474 | 4492 | use ln::features::InitFeatures; |
|
4475 | - | use ln::msgs::{OptionalField, DataLossProtect}; |
|
4493 | + | use ln::msgs::{OptionalField, DataLossProtect, DecodeError}; |
|
4476 | 4494 | use ln::chan_utils; |
|
4477 | 4495 | use ln::chan_utils::{ChannelPublicKeys, HolderCommitmentTransaction, CounterpartyChannelTransactionParameters}; |
|
4478 | 4496 | use chain::chaininterface::{FeeEstimator,ConfirmationTarget}; |
4528 | 4546 | self.chan_keys.clone() |
|
4529 | 4547 | } |
|
4530 | 4548 | fn get_secure_random_bytes(&self) -> [u8; 32] { [0; 32] } |
|
4549 | + | fn read_chan_signer(&self, _data: &[u8]) -> Result<Self::ChanKeySigner, DecodeError> { panic!(); } |
|
4531 | 4550 | } |
|
4532 | 4551 | ||
4533 | 4552 | fn public_from_secret_hex(secp_ctx: &Secp256k1<All>, hex: &str) -> PublicKey { |
Learn more Showing 1 files with coverage changes found.
lightning/src/ln/functional_tests.rs
Files | Coverage |
---|---|
lightning/src | -0.07% 91.29% |
Project Totals (37 files) | 91.29% |
ec4c44b
990d1de
c07b4de
45d4d26
0f5580a
4345aa8
c5fca8c
9c9c881