feat: close_dlc msg and cooperative close logic - #255
Conversation
There was a problem hiding this comment.
It's a good feature, though:
- please add an integration test so we can be confident it's working, I don't think it does right now
- The chain monitor is used for channels, I don't think what you implemented here will work
- I think the best way to go to close the contract on the party offering the cooperative close would be to stay in the
Confirmedstate, but then maybe have aclose_tx: Option<Transaction>field that you check withincheck_confirmed_contractso that you can mark the contract asPreClosed/Closed. It could also be a list if we assume that there could be multiple cooperative close offers (in case some are not accepted/processed).
|
Thanks for that review @Tibo-lg
Added to
Thanks for clarification, removed accordingly
Not sure what you had in mind here. Were you thinking of having I'm guessing you were thinking of adding it to |
- rm problematic CooperativeClose state that could persist indefinitely - fix state validation to expect Confirmed instead of Signed state - rm chain monitor usage (designed for channels, not standalone contracts) - add integration tests - rename verify_and_complete_cooperative_close to complete_cooperative_close - update storage and serialization to remove CooperativeClose variant Fixes issues identified in code review where CooperativeClose state could get stuck if counterparty never responds, and resolves transaction broadcast conflicts with chain monitor architecture.
22c22c6 to
9a41fd9
Compare
- replace manual Ok/Err match with .ok() method - fixes CI failure on clippy:manual_ok_err lint
07d6b11 to
92cbf5c
Compare
| }; | ||
|
|
||
| self.store | ||
| .update_contract(&Contract::Closed(closed_contract))?; |
| .expect("Error accepting cooperative close"); | ||
|
|
||
| // Bob should now be in Closed state (he broadcast the transaction) | ||
| assert_contract_state!(bob_manager_send, contract_id, Closed); |
There was a problem hiding this comment.
Should be PreClosed because the transaction is not confirmed.
| // In a real scenario, Alice would detect the close transaction and call on_counterparty_close | ||
| // For the test, we'll verify the cooperative close functionality worked correctly |
There was a problem hiding this comment.
We definitely want to detect the close transaction, and we want to test it.
There was a problem hiding this comment.
I've added check_pending_close_transactions function and included it in periodic_check https://github.com/p2pderivatives/rust-dlc/pull/255/files#diff-6901f71d060cd9a08d68e0e45ba8e40bcd714a041ddcdff6ea168eb4b35a1c4dR813-R863
Also updated tests https://github.com/p2pderivatives/rust-dlc/pull/255/files#diff-11a961d721d78a3c54a7b2a0b90b92b5a3768c0fbed6fef9c45014c0be8702e8R910-R936
I don't have a strong opinion on where to put it, I guess |
- add `pending_close_txs` field to track cooperative close offers - add `check_pending_close_transactions()` for chain monitoring - fix cooperative close flow: validate vs accept separation - implement proper state transitions: Confirmed → PreClosed → Closed - update test coverage for cooperative close detection - support self-initiated and counterparty-initiated closes Resolves automatic detection of cooperative close transactions that are broadcast by counterparties, ensuring both parties transition to the correct final state
Regenerated test files after adding pending_close_txs field to Dlctransactions. These serialized files were used by the sled storage provider tests to verify correct serialization/deserialization.
|
Thanks @Tibo-lg I added Also fixed proper state transitions, with Confirmed → PreClosed → Closed |
| pub fund_input_serial_id: u64, | ||
| /// The funding inputs to use. | ||
| pub funding_inputs: Vec<FundingInput>, | ||
| /// The funding signatures. | ||
| pub funding_signatures: FundingSignatures, |
There was a problem hiding this comment.
I think these fields are never used?
There was a problem hiding this comment.
These fields are necessary to ensure the offeror of the close contract can avoid a free option problem. They could offer a close to the counterparty, and the counterparty does not respond.
There was a problem hiding this comment.
I've added an additional optional argument for create_collaborative_close_transaction to include additional inputs in a915378
There was a problem hiding this comment.
Right now the way you set those fields does not make sense, you are setting them to the funding inputs and signatures, which matches with the names but not what you're describing here. If you want to be able to double spend the closing transaction then the inputs and signatures (actually a single one should be enough) should be set to utxos from the wallet. Honestly I feel the best is either you fully implement this (having it optional if you prefer), or just remove it.
| /// The signature for the closing transaction. | ||
| pub close_signature: Signature, | ||
| /// The payout amount for the offer party in satoshis. | ||
| pub offer_payout: Amount, |
There was a problem hiding this comment.
You don't need to pass that it can be re-computed from total_collateral
| Ok((close_message, counter_party)) | ||
| } | ||
|
|
||
| /// Accepts a cooperative close request by verifying the counter party's signature, |
There was a problem hiding this comment.
I don't think you are verifying the signature (not that you need to, since if it's incorrect the broadcast will fail)
| assert_contract_state!(alice_manager_send, contract_id, Closed); | ||
|
|
||
| // Verify the close transaction was properly broadcast and confirmed | ||
| let _close_txid = { |
There was a problem hiding this comment.
Remove this block if you don't need this
|
|
||
| periodic_check!(second, contract_id, Confirmed); | ||
| match path { | ||
| TestPath::Close => { |
There was a problem hiding this comment.
I would prefer that you don't refactor the rest of the tests to keep the PR focused, feel free to do another refactor PR.
| // Check if this is a cooperative close (no attestations) or a CET close (with attestations) | ||
| let (signed_cet, pnl) = if contract.attestations.is_none() { | ||
| // Cooperative close - no signed_cet in the final closed contract | ||
| let pnl = contract | ||
| .signed_contract | ||
| .accepted_contract | ||
| .compute_pnl(&contract.signed_cet)?; | ||
| (None, pnl) | ||
| } else { | ||
| // CET close - include the signed_cet | ||
| let pnl = contract | ||
| .signed_contract | ||
| .accepted_contract | ||
| .compute_pnl(&contract.signed_cet)?; | ||
| (Some(contract.signed_cet.clone()), pnl) | ||
| }; | ||
|
|
There was a problem hiding this comment.
Why do you differentiate here?
There was a problem hiding this comment.
Good point, no need to differentiate, fixed in 9d84b29
| } | ||
|
|
||
| /// Check for pending cooperative close transactions | ||
| fn check_pending_close_transactions(&self) -> Result<(), Error> { |
There was a problem hiding this comment.
This logic should be implemented within check_confirmed_contracts, unless there is a reason to have it separate? Right now that means having to go two times through the confirmed contracts.
| let pnl = contract.accepted_contract.compute_pnl(pending_close_tx)?; | ||
| let closed_contract = ClosedContract { | ||
| attestations: None, // Cooperative close has no attestations | ||
| signed_cet: None, // Cooperative close doesn't use a CET |
There was a problem hiding this comment.
Why set it to None, I think it's fine to save the closing transaction here, unless it breaks something?
There was a problem hiding this comment.
You're right, signed_cet can be the closing tx, which is must more elegant. Updated in 9d84b29
271662e to
ec6cb89
Compare
- rm offer_payout field as it can be computed from total_collateral - accept_payout - add fee_rate_per_vb field to prevent free option problem - update serialization accordingly
- rm differentiation between cooperative close and CET close in check_preclosed_contracts - update accept_cooperative_close comment to be more accurate about what it does - move pending close transaction logic into check_confirmed_contracts - save closing transaction instead of setting to None
- add additional_inputs parameter to offer_collaborative_close functions - update create_collaborative_close_transaction to accept additional inputs - add documentation explaining the free option problem prevention - keep funding inputs in CloseDlc message to prevent free option problem
- add test for collaborative close with additional inputs - update existing tests to use new API with additional_inputs parameter - add test for free option problem prevention - update test structure to reflect simplified cooperative close logic
- update close_msg.json test file to match new CloseDlc structure - rm offerPayout field and add feeRatePerVb field - clean up extra blank lines in manager.rs - fixes test failures caused by structure changes
What
Add
close_dlcmsg type, and cooperative close logicWhy
It allows Alice or Bob to pass a message with the intention of cooperatively closing the DLC, allowing for early exit from a position.
The initiator provides an input, so they can cancel if the counterparty doesn't respond right away.
Yes this creates a free option problem, but the expectation is that the counterparty is a trusted party (for example a market maker).
This message type was discussed in DLC Specs: discreetlogcontracts/dlcspecs#161
And has been implemented in the Atomic Finance typescript libraries: AtomicFinance/bitcoin-abstraction-layer#86 and AtomicFinance/node-dlc#122