Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(legacy-refunds): support going the spending path on refund #2280

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from

Conversation

mariocynicys
Copy link
Collaborator

This PR attempts to follow a more robust recovery process by attempting both refunding OR spending if the swap goes the ugly way.
This is done by replacing the refund-only logic with refund-or-spend-logic (recovery, recover_funds).

Also recover_funds has been adapted to return more structured error types for information about retrials and such. Also removing some pre-checks that makes us fail early based on local data (e.g. is_swap_finished), as we should still query the rpc to make sure our recovery tx isn't lost or something.

modularize these funcs and introdcue a new error type to filter errors and possible retrys.
tests are not yet adapted.
not finished and on successful checking swaps are removed. these tests were used to check that we error on recover funds when:
- swap is not finished yet: there is not reason not to try recover funds still
- swap was successful (determined the existance of taker/maker payment spend): we should still attempt to recover funds in this case as the tx might be re-orged or some weird thing happned. there is no downside of retrying.

also test_recover_funds_maker_swap_maker_payment_refunded was used to test that we check local data to determine that the swap failed. again this isn't the followed approach here. even if we store maker_payment_refund tx locally (so we should have sent it when the swap was running) we will still attempt to run recover funds and look for the tx on-chain.
this test could have been adapted but then it looks exactly like test_recover_funds_maker_payment_refund_already_refunded, so there is no point of that.
namely, we do try to refund (maker payment) or spend (taker payment), which ever is possible.
we might want to change refund_maker_payment name to recover or something
same as what's done with the maker.
thought much more useful here because on the maker side we can't really miss spending the taker's payment while getting our own payment spent :p
Copy link
Member

@borngraced borngraced left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, looks cleaner. My only note is regarding error handling.

},
Err(e) => return ERR!("Error {} when trying to find taker payment spend", e),
Err(e) => return Err(RecoverSwapError::Temporary(e)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we won't get error tracing anymore from this fn. Please return MmError<RecoverSwapError>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the returned error is a string. how would tracing work here? or you mean change the error from search_for_swap_tx_spend_other?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not just String, location is also prepended
https://docs.rs/gstuff/latest/gstuff/macro.ERR.html

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i see ERR! does prepend the current location, but i mean it doesn't get the trace, only the line in this function i think.
we can revert that though for the sake of RPC usage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but why not MmError? I mean instead of reverting?

Copy link
Member

@laruh laruh Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but why not MmError? I mean instead of reverting?

Legacy code doesnt support MmError, it requires String in a lot of places

Well @mariocynicys could use MmError for internal function and in general function, which returns String, convert mmerr to string, but its better to just come back to ERR macro and wrap errors in internal function

Copy link
Member

@borngraced borngraced Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's already an old issue to replace macros from gstuff with MmError generally..
#1250

You can simply map to String in the function caller if it requires returning String as Error

Copy link
Member

@laruh laruh Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that explicit error with enum variants in recover_funds function simplifies logic handled in refund payment functions

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but why not MmError? I mean instead of reverting?

ah sure, i forgot the original comment was for MmError xD

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if self.finished_at.load(Ordering::Relaxed) == 0 {
return ERR!("Swap must be finished before recover funds attempt");
}
pub async fn recover_funds(&self) -> Result<RecoveredSwap, RecoverSwapError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laruh
Copy link
Member

laruh commented Nov 25, 2024

@mariocynicys could you please fix PR lint

@mariocynicys mariocynicys changed the title optimization(legacy-swap): support going the spending path on refund fix(legacy-refunds): support going the spending path on refund Nov 25, 2024
Copy link
Member

@laruh laruh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this fix, here my notes

Comment on lines 1201 to 1235
// We recovered the swap successfully.
Ok(recovered_swap) => match recovered_swap {
// We recovered the swap by refunding the maker payment.
RecoveredSwap {
action: RecoveredSwapAction::RefundedMyPayment,
coin: _,
transaction,
} => {
let tx_ident = TransactionIdentifier {
tx_hex: transaction.tx_hex().into(),
tx_hash: transaction.tx_hash_as_bytes(),
};
info!("Maker payment refund tx {:02x}", tx_ident.tx_hash);
return Ok((Some(MakerSwapCommand::FinalizeMakerPaymentRefund), vec![
MakerSwapEvent::MakerPaymentRefunded(Some(tx_ident)),
]));
},
// We recovered the swap by proceeding forward and spending the taker payment. The swap wasn't actually a failure.
// Roll back to confirming the taker payment spend.
RecoveredSwap {
action: RecoveredSwapAction::SpentOtherPayment,
coin: _,
transaction,
} => {
let tx_ident = TransactionIdentifier {
tx_hex: transaction.tx_hex().into(),
tx_hash: transaction.tx_hash_as_bytes(),
};
info!("Refund canceled. Taker payment spend tx {:02x}", tx_ident.tx_hash);
// TODO: We prepared for refund but didn't finalize refund. This must be breaking something for lightning.
return Ok((Some(MakerSwapCommand::ConfirmTakerPaymentSpend), vec![
MakerSwapEvent::TakerPaymentSpent(tx_ident),
MakerSwapEvent::TakerPaymentSpendConfirmStarted,
]));
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be simplified using closure and return match

    // We recovered the swap successfully.
                Ok(recovered_swap) => {
                    let tx_ident = |tx: TransactionEnum| TransactionIdentifier {
                        tx_hex: tx.tx_hex().into(),
                        tx_hash: tx.tx_hash_as_bytes(),
                    };

                    // We recovered the swap by refunding the maker payment.
                    return match recovered_swap.action {
                        RecoveredSwapAction::RefundedMyPayment => {
                            let ident = tx_ident(recovered_swap.transaction);
                            info!("Maker payment refund tx {:02x}", ident.tx_hash);
                            Ok((Some(MakerSwapCommand::FinalizeMakerPaymentRefund), vec![
                                MakerSwapEvent::MakerPaymentRefunded(Some(ident)),
                            ]))
                        },
                        // We recovered the swap by proceeding forward and spending the taker payment. The swap wasn't actually a failure.
                        // Roll back to confirming the taker payment spend.
                        RecoveredSwapAction::SpentOtherPayment => {
                            let ident = tx_ident(recovered_swap.transaction);
                            info!("Refund canceled. Taker payment spend tx {:02x}", ident.tx_hash);
                            // TODO: We prepared for refund but didn't finalize refund. This must be breaking something for lightning.
                            Ok((Some(MakerSwapCommand::ConfirmTakerPaymentSpend), vec![
                                MakerSwapEvent::TakerPaymentSpent(ident),
                                MakerSwapEvent::TakerPaymentSpendConfirmStarted,
                            ]))
                        },
                    };
                },

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

},
// The swap will be automatically recovered after the specified locktime.
RecoverSwapError::AutoRecoverableAfter(locktime) => {
let maker_payment = self.r().maker_payment.clone().unwrap().tx_hex;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better use maker_payment.as_ref() and clone tx_hex

let maker_payment = self.r().maker_payment.as_ref().unwrap().tx_hex.clone()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 1912 to 1947
// We recovered the swap successfully.
Ok(recovered_swap) => match recovered_swap {
// We recovered the swap by refunding the taker payment.
RecoveredSwap {
action: RecoveredSwapAction::RefundedMyPayment,
coin: _,
transaction,
} => {
let tx_ident = TransactionIdentifier {
tx_hex: transaction.tx_hex().into(),
tx_hash: transaction.tx_hash_as_bytes(),
};
info!("Taker payment refund tx {:02x}", tx_ident.tx_hash);
return Ok((Some(TakerSwapCommand::FinalizeTakerPaymentRefund), vec![
TakerSwapEvent::TakerPaymentRefunded(Some(tx_ident)),
]));
},
// We recovered the swap by proceeding forward and spending the maker payment. The swap wasn't actually a failure.
// Roll back to confirming the maker payment spend.
RecoveredSwap {
action: RecoveredSwapAction::SpentOtherPayment,
coin: _,
transaction,
} => {
let tx_ident = TransactionIdentifier {
tx_hex: transaction.tx_hex().into(),
tx_hash: transaction.tx_hash_as_bytes(),
};
info!("Refund canceled. Taker payment spend tx {:02x}", tx_ident.tx_hash);
// TODO: We prepared for refund but didn't finalize refund. This must be breaking something for lightning.
// We better find a way to rollback the state machine and remove erroneous events,
// the swap at this point will be marked as errored but in fact it recovered from the error.
return Ok((Some(TakerSwapCommand::ConfirmMakerPaymentSpend), vec![
TakerSwapEvent::MakerPaymentSpent(tx_ident),
]));
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also can be simplified with closure and return match

                // We recovered the swap successfully.
                Ok(recovered_swap) => {
                    let tx_ident = |transaction: TransactionEnum| TransactionIdentifier {
                        tx_hex: transaction.tx_hex().into(),
                        tx_hash: transaction.tx_hash_as_bytes(),
                    };

                    return match recovered_swap.action {
                        // We recovered the swap by refunding the taker payment.
                        RecoveredSwapAction::RefundedMyPayment => {
                            let ident = tx_ident(recovered_swap.transaction);
                            info!("Taker payment refund tx {:02x}", ident.tx_hash);
                            Ok((Some(TakerSwapCommand::FinalizeTakerPaymentRefund), vec![
                                TakerSwapEvent::TakerPaymentRefunded(Some(ident)),
                            ]))
                        },
                        // We recovered the swap by proceeding forward and spending the maker payment. The swap wasn't actually a failure.
                        // Roll back to confirming the maker payment spend.
                        RecoveredSwapAction::SpentOtherPayment => {
                            let ident = tx_ident(recovered_swap.transaction);
                            info!("Refund canceled. Taker payment spend tx {:02x}", ident.tx_hash);
                            // TODO: We prepared for refund but didn't finalize refund. This must be breaking something for lightning.
                            // We better find a way to rollback the state machine and remove erroneous events,
                            // the swap at this point will be marked as errored but in fact it recovered from the error.
                            Ok((Some(TakerSwapCommand::ConfirmMakerPaymentSpend), vec![
                                TakerSwapEvent::MakerPaymentSpent(ident),
                            ]))
                        },
                    };
                },

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aha i see,
you could also run tx_ident outside the match since it's the exact same code for both branches.
i did it this way though since it fits more with the style of the error routes so i thought it will be easier to read. sometimes i lean for readability & style and ignore other aspects :)
but sense u mentioned it, i guess i looks off, so let's combine the combinable code outside the match then.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

},
// The swap will be automatically recovered after the specified locktime.
RecoverSwapError::AutoRecoverableAfter(locktime) => {
let taker_payment = self.r().taker_payment.clone().unwrap().tx_hex;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its better to use as_ref for taker_payment

let taker_payment = self.r().taker_payment.as_ref().unwrap().tx_hex.clone();

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pub async fn recover_funds(&self) -> Result<RecoveredSwap, String> {
async fn try_spend_taker_payment(selfi: &MakerSwap, secret_hash: &[u8]) -> Result<TransactionEnum, String> {
pub async fn recover_funds(&self) -> Result<RecoveredSwap, RecoverSwapError> {
async fn try_spend_taker_payment(selfi: &MakerSwap) -> Result<TransactionEnum, RecoverSwapError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, why not move try_spend_taker_payment function out of the recover_funds function?

Right now, there is a large function nested inside another large function, and both only require &self.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this scopes the helper function better since it's not used elsewhere. we can consider putting helpers on the impl Struct scope if that impl scope doesn't have too many methods already, but this one has tons of methods so scoping the helper inside recover_funds would make more sense.

@laruh laruh added in progress Changes will be made from the author and removed under review labels Nov 29, 2024
@mariocynicys mariocynicys added under review and removed in progress Changes will be made from the author labels Nov 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants