Skip to content

Commit

Permalink
Backend(LN),Frontend.Console,End2End: support CPFP on mutual close tx
Browse files Browse the repository at this point in the history
ConfirmRecoveryTxFee name update was needed because it's no longer
only used for recovery tx, it's now used for CPFP TXs as well.
  • Loading branch information
aarani authored and knocte committed Aug 11, 2022
1 parent 63c100a commit 00469bc
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 31 deletions.
1 change: 1 addition & 0 deletions scripts/make.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ let RunTests (suite: string) =
"ChannelRemoteForceClosingByFundee"
"Revocation"
"CPFP"
"MutualCloseCpfp"
"UpdateFeeMsg"
]

Expand Down
116 changes: 110 additions & 6 deletions src/GWallet.Backend.Tests.End2End/LN.fs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ type LN() =
| ChannelStatus.Closing -> ()
| status -> return failwith (SPrintF1 "unexpected channel status. Expected Closing, got %A" status)

// Give fundee time to see the closing tx on blockchain
do! Async.Sleep 10000

// Mine 10 blocks to make sure closing tx is confirmed
bitcoind.GenerateBlocksToDummyAddress (BlockHeightOffset32 (uint32 10))

Expand All @@ -164,10 +167,11 @@ type LN() =
if attempt = 10 then
return Error "Closing tx not confirmed after maximum attempts"
else
let! txIsConfirmed = Lightning.Network.CheckClosingFinished clientWallet.ChannelStore channelId
if txIsConfirmed then
let! closingTxResult = Lightning.Network.CheckClosingFinished clientWallet.ChannelStore channelId
match closingTxResult with
| Tx (Full, _closingTx) ->
return Ok ()
else
| _ ->
do! Async.Sleep 1000
return! waitForClosingTxConfirmed (attempt + 1)
}
Expand Down Expand Up @@ -517,10 +521,11 @@ type LN() =
if attempt = 10 then
return Error "Closing tx not confirmed after maximum attempts"
else
let! txIsConfirmed = Lightning.Network.CheckClosingFinished clientWallet.ChannelStore channelId
if txIsConfirmed then
let! closingTxResult = Lightning.Network.CheckClosingFinished clientWallet.ChannelStore channelId
match closingTxResult with
| Tx (Full, _closingTx) ->
return Ok ()
else
| _ ->
do! Async.Sleep 1000
return! waitForClosingTxConfirmed (attempt + 1)
}
Expand Down Expand Up @@ -1496,3 +1501,102 @@ type LN() =
(serverWallet :> IDisposable).Dispose()
}

[<Category "G2G_MutualCloseCpfp_Funder">]
[<Test>]
member __.``can CPFP on mutual close (funder)``() = Async.RunSynchronously <| async {
let! channelId, clientWallet, bitcoind, electrumServer, lnd, fundingAmount =
try
OpenChannelWithFundee (Some Config.FundeeNodeEndpoint)
with
| ex ->
Assert.Inconclusive (
sprintf
"Channel-closing inconclusive because Channel open failed, fix this first: %s"
(ex.ToString())
)
failwith "unreachable"

try
do! SendMonoHopPayments clientWallet channelId fundingAmount
with
| ex ->
Assert.Inconclusive (
sprintf
"Channel-closing inconclusive because sending of monohop payments failed, fix this first: %s"
(ex.ToString())
)
failwith "unreachable"

do! ClientCloseChannel clientWallet bitcoind channelId

TearDown clientWallet bitcoind electrumServer lnd
}


[<Category "G2G_MutualCloseCpfp_Fundee">]
[<Test>]
member __.``can CPFP on mutual close (fundee)``() = Async.RunSynchronously <| async {
let! serverWallet, channelId = AcceptChannelFromGeewalletFunder ()

do! ReceiveMonoHopPayments serverWallet channelId

let! closeChannelRes = Lightning.Network.AcceptCloseChannel serverWallet.NodeServer channelId
match closeChannelRes with
| Ok _ -> ()
| Error err -> return failwith (SPrintF1 "failed to accept close channel: %A" err)

let! oldFeeRate = ElectrumServer.EstimateFeeRate()

let newFeeRate = oldFeeRate * 5u
ElectrumServer.SetEstimatedFeeRate newFeeRate

let rec waitForClosingTxConfirmed attempt = async {
Infrastructure.LogDebug (SPrintF1 "Checking if closing tx is finished, attempt #%d" attempt)
if attempt = 10 then
return Error "Closing tx not confirmed after maximum attempts"
else
let! closingTxResult = Lightning.Network.CheckClosingFinished serverWallet.ChannelStore channelId
match closingTxResult with
| Tx (WaitingForFirstConf, closingTx) ->
let! cpfpCreationRes = ChannelManager.CreateCpfpTxOnMutualClose serverWallet.ChannelStore channelId closingTx serverWallet.Password
match cpfpCreationRes with
| Ok mutualCpfp ->
return Ok (closingTx, mutualCpfp)
| _ -> return Error "CPFP tx creation failed"
| Tx (Full, _) ->
Assert.Inconclusive "Closing tx got confirmed before we get a chance to create CPFP tx"
return Error "Closing tx got confirmed before we get a chance to create CPFP tx"
| _ ->
do! Async.Sleep 1000
return! waitForClosingTxConfirmed (attempt + 1)
}

let! closingTxConfirmedRes = waitForClosingTxConfirmed 0
match closingTxConfirmedRes with
| Ok (mutualCloseTx, mutualCpfpTx) ->
let closingTx = Transaction.Parse(mutualCloseTx.Tx.ToString(), Network.RegTest)
let! closingTxFee = FeesHelper.GetFeeFromTransaction closingTx
let closingTxFeeRate =
FeeRatePerKw.FromFeeAndVSize(closingTxFee, uint64 (closingTx.GetVirtualSize()))
assert FeesHelper.FeeRatesApproxEqual closingTxFeeRate oldFeeRate

let cpfpTx = Transaction.Parse(mutualCpfpTx.Tx.ToString(), Network.RegTest)
let! txFeeWithCpfp = FeesHelper.GetFeeFromTransaction cpfpTx
let txFeeRateWithCpfp =
FeeRatePerKw.FromFeeAndVSize(txFeeWithCpfp, uint64 (cpfpTx.GetVirtualSize()))
assert (not <| FeesHelper.FeeRatesApproxEqual txFeeRateWithCpfp oldFeeRate)
assert (not <| FeesHelper.FeeRatesApproxEqual txFeeRateWithCpfp newFeeRate)
let combinedFeeRateWithCpfp =
FeeRatePerKw.FromFeeAndVSize(
txFeeWithCpfp + closingTxFee,
uint64 (closingTx.GetVirtualSize() + cpfpTx.GetVirtualSize())
)
assert FeesHelper.FeeRatesApproxEqual combinedFeeRateWithCpfp newFeeRate

let! _cpfpTxId = Account.BroadcastRawTransaction serverWallet.Account.Currency (mutualCpfpTx.Tx.ToString())

return ()
| Error err -> return failwith (SPrintF1 "error when waiting for closing tx to confirm: %s" err)

(serverWallet :> IDisposable).Dispose()
}
6 changes: 6 additions & 0 deletions src/GWallet.Backend.Tests.Unit/ChannelMarshalling.fs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,12 @@ type ChannelMarshalling () =
"case": "Tor"
}
]
},
"closingTimestampUtc": {
"case": "Some",
"fields": [
1647341733
]
}
}
}
Expand Down
101 changes: 101 additions & 0 deletions src/GWallet.Backend/UtxoCoin/Lightning/ChannelManagement.fs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ type ChannelStore(account: NormalUtxoAccount) =
let serializedChannel = self.LoadChannel channelId
serializedChannel.SavedChannelState.LocalCommit.PublishableTxs.CommitTx.Value.ToHex()

member self.TryGetClosingTimestampUtc (channelId: ChannelIdentifier): Option<DateTime> =
let serializedChannel = self.LoadChannel channelId
serializedChannel.ClosingTimestampUtc

member self.GetToSelfDelay (channelId: ChannelIdentifier): uint16 =
let serializedChannel = self.LoadChannel channelId
serializedChannel.SavedChannelState.StaticChannelConfig.LocalParams.ToSelfDelay.Value
Expand Down Expand Up @@ -420,3 +424,100 @@ module ChannelManager =
else
return false
}

type MutualCloseCpfpCreationError =
| BalanceBelowDustLimit
| NotEnoughFundsForFees

let CreateCpfpTxOnMutualClose
(channelStore: ChannelStore)
(channelId: ChannelIdentifier)
(closingTx: MutualCloseTx)
(password: string)
: Async<Result<MutualCloseCpfp, MutualCloseCpfpCreationError>> =
async {
let account = channelStore.Account
//FIXME: this might crash with ReadOnly accounts, we should make sure
// ReadOnly is simply not supported instead of crashing
let privateKey = Account.GetPrivateKey account password
let serializedChannel = channelStore.LoadChannel channelId
let currency = channelStore.Currency
let network = UtxoCoin.Account.GetNetwork currency
let targetAddress =
let originAddress = (account :> IAccount).PublicAddress
BitcoinAddress.Create(originAddress, network)
let! feeRate = async {
let! feeEstimator = FeeEstimator.Create currency
return feeEstimator.FeeRatePerKw
}

let localShutdownScriptPubKey =
UnwrapOption serializedChannel.NegotiatingState.LocalRequestedShutdown "BUG: local shutdown script is empty"

let localOutputOpt =
closingTx.Tx.NBitcoinTx.Outputs
|> Seq.tryFind (fun output -> output.ScriptPubKey = localShutdownScriptPubKey.ScriptPubKey())

match localOutputOpt with
| Some localOutput ->
let transactionBuilder = network.CreateTransactionBuilder()
let publicKeyWitHash = (account :> IUtxoAccount).PublicKey.WitHash.ScriptPubKey
let scriptCoin = ScriptCoin(closingTx.Tx.NBitcoinTx, localOutput, publicKeyWitHash)

transactionBuilder.AddKeys privateKey |> ignore<TransactionBuilder>
transactionBuilder.AddCoin scriptCoin |> ignore<TransactionBuilder>
transactionBuilder.SendAll targetAddress |> ignore<TransactionBuilder>
try
let fee =
FeeEstimator.EstimateCpfpFee
transactionBuilder
feeRate
closingTx.Tx.NBitcoinTx
(serializedChannel.FundingScriptCoin())
transactionBuilder.SendFees fee |> ignore

let cpfpTransaction: MutualCloseCpfp =
{
ChannelId = channelId
Currency = currency
Fee = MinerFee (fee.Satoshi, DateTime.UtcNow, currency)
Tx =
{
NBitcoinTx = transactionBuilder.BuildTransaction true
}
}
return Ok cpfpTransaction
with
| :? NotEnoughFundsException ->
return Error NotEnoughFundsForFees
| None ->
return Error BalanceBelowDustLimit
}

let IsCpfpNeededForFundingSpendingTx
(channelStore: ChannelStore)
(channelId: ChannelIdentifier)
(spendingTxId: TransactionIdentifier)
=
async {
let channel = channelStore.LoadChannel channelId
let fundingScriptCoin = channel.FundingScriptCoin()
let currency = channelStore.Currency
let network = UtxoCoin.Account.GetNetwork currency
let! spendingTxString =
Server.Query
currency
(QuerySettings.Default ServerSelectionMode.Fast)
(ElectrumClient.GetBlockchainTransaction (spendingTxId.ToString()))
None
let spendingTxFeeRate =
let spendingTx = Transaction.Parse(spendingTxString, network)
spendingTx.GetFeeRate (Array.singleton (fundingScriptCoin :> ICoin))

let! currentFeeRate = async {
let! feeEstimator = FeeEstimator.Create currency
return feeEstimator.FeeRatePerKw.AsNBitcoinFeeRate()
}

return spendingTxFeeRate.FeePerK < currentFeeRate.FeePerK
}
23 changes: 19 additions & 4 deletions src/GWallet.Backend/UtxoCoin/Lightning/ClosedChannel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ type internal CloseChannelError =
| ExpectedClosingSignedMsg _ -> false
| ApplyClosingSignedFailed _ -> true

type ConfirmationStatus =
| Full
| InProgress
| WaitingForFirstConf

type ClosureTransaction =
| DidNotHappenYet
| Tx of ConfirmationStatus * MutualCloseTx


(*
+-------+ +-------+
| |--(1)----- shutdown ------->| |
Expand Down Expand Up @@ -334,6 +344,7 @@ type ClosedChannel()=
{
Channel = channelAfterApplyClosingSigned
}
ClosingTimestampUtc = Some DateTime.UtcNow
}

connectedChannelAfterMutualClosePerformed.SaveToWallet()
Expand All @@ -353,15 +364,19 @@ type ClosedChannel()=
static member internal CheckClosingFinished
(channelStore: ChannelStore)
(channelId: ChannelIdentifier)
: Async<bool>
: Async<ClosureTransaction>
=
async {
let! closingTxOpt = channelStore.CheckForClosingTx channelId
match closingTxOpt with
| Some (ClosingTx.MutualClose _closingTx, Some closingTxConfirmations) when
| Some (ClosingTx.MutualClose closingTx, Some closingTxConfirmations) when
BlockHeightOffset32 closingTxConfirmations >= Settings.DefaultTxMinimumDepth channelStore.Currency ->
channelStore.ArchiveChannel channelId
return true
return Tx (ConfirmationStatus.Full, closingTx)
| Some (ClosingTx.MutualClose closingTx, Some _closingTxConfirmations) ->
return Tx (ConfirmationStatus.InProgress, closingTx)
| Some (ClosingTx.MutualClose closingTx, None) ->
return Tx (ConfirmationStatus.WaitingForFirstConf, closingTx)
| _ ->
return false
return DidNotHappenYet
}
4 changes: 4 additions & 0 deletions src/GWallet.Backend/UtxoCoin/Lightning/ConnectedChannel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type internal ConnectedChannel =
Account: NormalUtxoAccount
MinimumDepth: BlockHeightOffset32
ChannelIndex: int
ClosingTimestampUtc: Option<DateTime>
}
interface IDisposable with
member self.Dispose() =
Expand Down Expand Up @@ -191,6 +192,7 @@ type internal ConnectedChannel =
PeerNode = peerNodeAfterReestablish
MinimumDepth = minimumDepth
ChannelIndex = channelIndex
ClosingTimestampUtc = serializedChannel.ClosingTimestampUtc
}
return Ok connectedChannel
}
Expand Down Expand Up @@ -224,6 +226,7 @@ type internal ConnectedChannel =
PeerNode = peerNodeAfterReestablish
MinimumDepth = minimumDepth
ChannelIndex = channelIndex
ClosingTimestampUtc = serializedChannel.ClosingTimestampUtc
}
return Ok connectedChannel
}
Expand All @@ -242,6 +245,7 @@ type internal ConnectedChannel =
LocalChannelPubKeys = self.Channel.ChannelPrivKeys.ToChannelPubKeys()
NodeTransportType = self.PeerNode.NodeTransportType
RecoveryTxIdOpt = None
ClosingTimestampUtc = self.ClosingTimestampUtc
}
channelStore.SaveChannel serializedChannel

Expand Down
2 changes: 2 additions & 0 deletions src/GWallet.Backend/UtxoCoin/Lightning/FundedChannel.fs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ type internal FundedChannel =
Account = account
MinimumDepth = outgoingUnfundedChannel.MinimumDepth
ChannelIndex = outgoingUnfundedChannel.ChannelIndex
ClosingTimestampUtc = None
}
connectedChannel.SaveToWallet()
let! _txid =
Expand Down Expand Up @@ -241,6 +242,7 @@ type internal FundedChannel =
Account = account
MinimumDepth = minimumDepth
ChannelIndex = channelIndex
ClosingTimestampUtc = None
}
connectedChannelAfterFundingCreated.SaveToWallet()

Expand Down
4 changes: 2 additions & 2 deletions src/GWallet.Backend/UtxoCoin/Lightning/Node.fs
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ type Node =
transactionBuilder.SendAll targetAddress |> ignore
let fee = transactionBuilder.EstimateFees (feeRate.AsNBitcoinFeeRate())
transactionBuilder.SendFees fee |> ignore
let recoveryTransaction =
let recoveryTransaction: RecoveryTx =
{
ChannelId = channelId
Currency = currency
Expand Down Expand Up @@ -809,7 +809,7 @@ type Node =
else
transactionBuilder.EstimateFees (feeRate.AsNBitcoinFeeRate())
transactionBuilder.SendFees fee |> ignore
let recoveryTransaction =
let recoveryTransaction: RecoveryTx =
{
ChannelId = channelId
Currency = currency
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type SerializedChannel =
LocalChannelPubKeys: ChannelPubKeys
RecoveryTxIdOpt: Option<TransactionIdentifier>
NodeTransportType: NodeTransportType
ClosingTimestampUtc: Option<DateTime>
}
static member LightningSerializerSettings currency: JsonSerializerSettings =
let settings = JsonMarshalling.SerializerSettings
Expand Down
Loading

0 comments on commit 00469bc

Please sign in to comment.