[r2r] Lightning channels and payments history#1240
Conversation
…o sql in the future
artemii235
left a comment
There was a problem hiding this comment.
Few changes requests and questions 🙂
| .any(|info| info.txid == tx_info.tx.txid()) | ||
| { | ||
| let rpc_txid = H256Json::from(tx_info.tx.txid().as_hash().into_inner()).reversed(); | ||
| let index = ok_or_retry_after_sleep!( |
There was a problem hiding this comment.
It seems that it's worth creating RetryableFuture instead of using macro. So it will look like
client
.blockchain_transaction_get_merkle(rpc_txid, tx_info.block_height)
.compat()
.retry_every(TRY_LOOP_INTERVAL)
.await;There was a problem hiding this comment.
I think this can be moved to the next PR as I mentioned here #1240 (comment) since this might be part of the code that will be changed. If ok_or_retry_after_sleep is to be used I will look into creating RetryableFuture instead for sure :)
There was a problem hiding this comment.
Ok, I added it to the issue checklist #1045. Please also update it if something is done or not actual anymore, etc.
There was a problem hiding this comment.
Please also update it if something is done or not actual anymore, etc.
Sure, I always do that, especially after a related PR merge to dev. For unresolved comments like this, I usually just reference them in the next PR that resolves them as I did here #1240 (comment).
sergeyboyko0791
left a comment
There was a problem hiding this comment.
Thanks for the huge fixes!
Second review iteration 😅
| let claiming_txid = spending_tx.txid().to_string(); | ||
| let persister = self.persister.clone(); | ||
| spawn(async move { | ||
| ok_or_retry_after_sleep!( |
There was a problem hiding this comment.
Should this macro be used elsewhere in ln_events.rs? Am I right this is only one critical section if you decided to use the macro?
There was a problem hiding this comment.
Am I right this is only one critical section if you decided to use the macro?
This is related to this comment #1240 (comment)
| let mut confirmed_registered_txs = Vec::new(); | ||
| for (txid, scripts) in registered_txs { | ||
| if let Some(transaction) = | ||
| ok_or_continue_after_sleep!(self.get_tx_if_onchain(txid).await, TRY_LOOP_INTERVAL) |
There was a problem hiding this comment.
Is it needed to sleep before continue? If I'm not mistaken, in this case we sleep for TRY_LOOP_INTERVAL and just skip this transaction.
There was a problem hiding this comment.
My thought process was that if there was an error it would mostly be because there is a problem connecting to electrums (internet connection down, electrums down, etc..), and waiting a bit might be better than continuing before reconnecting.
If we didn't wait, this might lead to passing through all the registered_txs transactions in this loop without confirming any and waiting for the next block to confirm it, I actually encountered this case while testing when my internet went down for a few minutes.
I realize now that there should be a big change in the confirmation process of registered_txs and the watching for spending of registered_outputs and that I should revise this whole process. I have a few ideas to solve this problem but I think this should be implemented in another PR.
One of these ideas is once a transaction or an output is registered through the register functions https://github.com/KomodoPlatform/atomicDEX-API/blob/9b3339a7e6e2e49ae98345de6162b4d6c31cbfed/mm2src/coins/lightning/ln_platform.rs#L527 https://github.com/KomodoPlatform/atomicDEX-API/blob/9b3339a7e6e2e49ae98345de6162b4d6c31cbfed/mm2src/coins/lightning/ln_platform.rs#L530 I should just spawn a separate thread for this transaction/output while retrying each step that involves calls to electrums until confirmation. One problem with that approach is this error for instance #1240 (comment) where if a call fails for a reason unrelated to electrum connections, knowing that a certain output is spent might not resolve due to getting stuck in retrying such calls forever. This watched output might be related to an old commitment transaction broadcasted by a channel's malicious counterparty and we will not be able to broadcast a transaction to punish this counterparty for broadcasting an old commitment transaction without detecting the spending of the channel opening transaction by this old commitment transaction, what makes this worse is that there is a window for broadcasting this punishing transaction and if passed some of our funds will be stolen.
These problems can all be solved after watchtowers are implemented since watchtowers should be running a full node and if a call to electrums fails for whatever reason we can delegate the watching for a certain transaction or a spending of an output to a number of watchtowers while providing them with the transactions that should be broadcasted in such cases.
I think the left unresolved comments are related to the above problem and should be resolved in a new PR since this code might be changed. The ok_or_retry_after_sleep was used for instance in here #1240 (comment) to avoid the above situation.
There was a problem hiding this comment.
The idea with the spawned thread or just a future sounds good! This will allow us to avoid infinite loop and to be sure that we'll try to broadcast transactions until they're accepted.
Just only one thing - I think that one separated thread is enough for this :D
I should just spawn a separate thread for this transaction/output
There was a problem hiding this comment.
Just only one thing - I think that one separated thread is enough for this :D
Big lightning nodes can have a lot of open channels, the most connected nodes have over 2000 open channels with them, which in turn means that they can watch for more than 2000 outputs if they are spent or not with every new block. Of course, these nodes should always use a full node as their chain source and not electrums, but I think it's the right way to at least spawn a thread for multiple transactions at a time but not all so as not to take a long time to finish the requests when a new block connects if a user who uses electrum decided to open a lot of channels.
| for (_, vout) in transaction.output.iter().enumerate() { | ||
| if scripts.contains(&vout.script_pubkey) { | ||
| let script_hash = hex::encode(electrum_script_hash(vout.script_pubkey.as_ref())); | ||
| let history = ok_or_retry_after_sleep!( |
There was a problem hiding this comment.
We can never return from this function if scripthash_get_history always fails. One particular case is when the RPC fails with history too large
https://github.com/KomodoPlatform/atomicDEX-API/blob/322dcbdeea76336dc699056b7f4f8f53476793c4/mm2src/coins/utxo/utxo_common.rs#L50-L53
Even if there is a network error, it's worth to return from the function with an error. How about adding a number of attemptions?
There was a problem hiding this comment.
I think this should be fixed in the next PR, please refer to #1240 (comment)
Also, I think the history too large error will not be returned from this since the script hashes are mostly for P2WSH addresses which are used once. I guess in the confirmation of the claiming transaction this error might appear since we claim back to our address and the script_pubkey of our address will be used in the scripthash_get_history, I guess I should find a solution for this but as I said this code might change and spawning these calls might be one of the solutions.
There was a problem hiding this comment.
Got it! There can be other permanent errors like invalid Electrum state etc.
There was a problem hiding this comment.
I think a good solution for electrum errors can be to request electrums sequentially if there is an error returned, so if the user is connected to 3 electrums if an error is returned from one then the request should be retried to the next electrum in the list. what do you think?
There was a problem hiding this comment.
I think a good solution for electrum errors can be to request electrums sequentially
Actually, all electrum requests are sequential through the rpc_func! macro, I guess I just forgot that :)
| if item.tx_hash == rpc_txid && item.height > 0 { | ||
| let height = item.height as u64; | ||
| let header = | ||
| ok_or_retry_after_sleep!(get_block_header(client, height).await, TRY_LOOP_INTERVAL); |
There was a problem hiding this comment.
I think this should be fixed in the next PR, please refer to #1240 (comment)
| destination: row | ||
| .get::<_, String>(1) | ||
| .ok() | ||
| .map(|d| PublicKey::from_str(&d).expect("PublicKey from str should not fail!")), |
There was a problem hiding this comment.
It can fail easily if the user change this field in the database 😄
Could you please make sure if it's safe to unwrap this and other lines in this function/file?
|
|
||
| let sql = "UPDATE ".to_owned() | ||
| + &table_name | ||
| + " SET funding_tx = ?2, funding_value = ?3, funding_generated_in_block = ?4, last_updated = ?5 WHERE rpc_id = ?1;"; |
There was a problem hiding this comment.
I think such order may help and confuse the user at the same time.
How about ordering the query params or adding a # Usage comment about the order?
Same question for other places in the file.
| async_blocking(move || { | ||
| let conn = selfi.0.lock().unwrap(); | ||
| query_single_row(&conn, &sql, params, tx_details_from_row) | ||
| query_single_row(&conn, &sql, params, tx_details_from_row).map_err(From::from) |
There was a problem hiding this comment.
Not critical at all, but I prefer to specify the target type to make the code clearer
query_single_row(&conn, &sql, params, tx_details_from_row).map_err(SqlError::from)
onur-ozkan
left a comment
There was a problem hiding this comment.
All looks good to me, just couple of minor suggestions. 🙂
(please also consider this suggestion from Artem's PR.)
sergeyboyko0791
left a comment
There was a problem hiding this comment.
Last review iteration.
There are no critical comments, so If it will take some time to implement, I think they can be postponed to the next impl iteration.
| PaymentType::OutboundPayment { | ||
| destination: PublicKey::from_str(&row.get::<_, String>(1)?).expect("PublicKey from str should not fail!"), | ||
| destination: PublicKey::from_str(&row.get::<_, String>(1)?) | ||
| .map_err(|e| SqlError::FromSqlConversionFailure(1, SqlType::Text, Box::new(e)))?, |
There was a problem hiding this comment.
Creating such error is repeating in this file. I have several ideas how it can be simplified:
- Return a custom error type:
enum MmSqlError {
ErrorParsing {
// Both or one of these:
field_name: String,
field_id: usize,
error: String,
}
}
impl LnStorageError {
pub fn error_parsing<E>(field_name: String, field_id: usize, error: E) -> Self where E: Error
{
MmSqlError::ErrorParsing {field_name, field_id, error: error.to_string()}
}
}- Use a function like:
pub fn sql_text_conversion_err<E>(field_id: usize, e: E) -> SqlError {
SqlError::FromSqlConversionFailure(field_id, SqlType::Text, Box::new(e))
}What do you think?
There was a problem hiding this comment.
pub fn sql_text_conversion_err<E>(field_id: usize, e: E) -> SqlError { SqlError::FromSqlConversionFailure(field_id, SqlType::Text, Box::new(e)) }
I prefer this idea since this function can be used easily for any such error. I will add it to our db_common lib.
| let mut hash_slice = [0u8; 32]; | ||
| hex::decode_to_slice(row.get::<_, String>(0)?, &mut hash_slice as &mut [u8]) | ||
| .map_err(|e| SqlError::FromSqlConversionFailure(0, SqlType::Text, Box::new(e)))?; | ||
| let payment_hash = PaymentHash(hash_slice); |
There was a problem hiding this comment.
Do you think this code snippet will be used elsewhere? How about adding a separate function like fn payment_hash_from_row(row, column_id)?
Same question for the below fields :)
sergeyboyko0791
left a comment
There was a problem hiding this comment.
Thank you for the fixes! Approve!
#1045