diff --git a/GWallet.Backend.nuspec b/GWallet.Backend.nuspec
index a93db2da4..174f6e8e6 100644
--- a/GWallet.Backend.nuspec
+++ b/GWallet.Backend.nuspec
@@ -10,7 +10,7 @@
false
geewallet is a minimalistic and pragmatist crossplatform lightweight opensource brainwallet for people that want to hold the most important cryptocurrencies in the same application with ease and peace of mind.
-
+
diff --git a/src/GWallet.Backend.Tests.End2End/GWallet.Backend.Tests.End2End.fsproj b/src/GWallet.Backend.Tests.End2End/GWallet.Backend.Tests.End2End.fsproj
index cc05aa99a..24741de3d 100644
--- a/src/GWallet.Backend.Tests.End2End/GWallet.Backend.Tests.End2End.fsproj
+++ b/src/GWallet.Backend.Tests.End2End/GWallet.Backend.Tests.End2End.fsproj
@@ -135,13 +135,13 @@
..\..\packages\BTCPayServer.Lightning.Ptarmigan.1.2.2\lib\netstandard2.0\BTCPayServer.Lightning.Ptarmigan.dll
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\DotNetLightning.Core.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\DotNetLightning.Core.dll
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\InternalBech32Encoder.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\InternalBech32Encoder.dll
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\ResultUtils.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\ResultUtils.dll
..\..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll
diff --git a/src/GWallet.Backend.Tests.End2End/LN.fs b/src/GWallet.Backend.Tests.End2End/LN.fs
index 6a4e15f82..724e5850c 100644
--- a/src/GWallet.Backend.Tests.End2End/LN.fs
+++ b/src/GWallet.Backend.Tests.End2End/LN.fs
@@ -833,7 +833,6 @@ type LN() =
(Node.Server serverWallet.NodeServer).CreateRecoveryTxForRemoteForceClose
channelId
closingTx
- false
| _ ->
do! Async.Sleep 2000
return! waitForRemoteForceClose()
@@ -899,7 +898,6 @@ type LN() =
(Node.Client clientWallet.NodeClient).CreateRecoveryTxForRemoteForceClose
channelId
forceCloseTx
- false
let recoveryTx = UnwrapResult recoveryTxOpt "no funds could be recovered"
let! _recoveryTxId =
ChannelManager.BroadcastRecoveryTxAndCloseChannel recoveryTx clientWallet.ChannelStore
@@ -1294,6 +1292,30 @@ type LN() =
FeeRatePerKw.FromFeeAndVSize(commitmentTxFee, uint64 (commitmentTx.GetVirtualSize()))
assert FeesHelper.FeeRatesApproxEqual commitmentTxFeeRate oldFeeRate
+ let! anchorTxRes =
+ (Node.Client clientWallet.NodeClient).CreateAnchorFeeBumpForForceClose
+ channelId
+ (commitmentTx.ToHex())
+ clientWallet.Password
+ let anchorTxString =
+ UnwrapResult
+ anchorTxRes
+ "force close failed to recover funds from the commitment tx"
+ let anchorTx = Transaction.Parse(anchorTxString.Tx.ToString(), Network.RegTest)
+ let! anchorTxFee = FeesHelper.GetFeeFromTransaction anchorTx
+ let anchorTxFeeRate =
+ FeeRatePerKw.FromFeeAndVSize(anchorTxFee, uint64 (anchorTx.GetVirtualSize()))
+ assert (not <| FeesHelper.FeeRatesApproxEqual anchorTxFeeRate oldFeeRate)
+ assert (not <| FeesHelper.FeeRatesApproxEqual anchorTxFeeRate newFeeRate)
+ let combinedFeeRate =
+ FeeRatePerKw.FromFeeAndVSize(
+ anchorTxFee + commitmentTxFee,
+ uint64 (anchorTx.GetVirtualSize() + commitmentTx.GetVirtualSize())
+ )
+ assert FeesHelper.FeeRatesApproxEqual combinedFeeRate newFeeRate
+
+ let! _anchorTxIdString = Account.BroadcastRawTransaction Currency.BTC (anchorTxString.Tx.ToString())
+
// Give the fundee time to see the force-close tx
do! Async.Sleep 5000
@@ -1350,50 +1372,6 @@ type LN() =
FeeRatePerKw.FromFeeAndVSize(forceCloseTxFee, uint64 (forceCloseTx.GetVirtualSize()))
assert FeesHelper.FeeRatesApproxEqual forceCloseTxFeeRate oldFeeRate
- let! recoveryTxStringNoCpfpRes =
- (Node.Server serverWallet.NodeServer).CreateRecoveryTxForRemoteForceClose
- channelId
- wrappedForceCloseTx
- false
- let recoveryTxStringNoCpfp =
- UnwrapResult
- recoveryTxStringNoCpfpRes
- "force close failed to recover funds from the commitment tx"
- let recoveryTxNoCpfp = Transaction.Parse(recoveryTxStringNoCpfp.Tx.ToString(), Network.RegTest)
- let! recoveryTxFeeNoCpfp = FeesHelper.GetFeeFromTransaction recoveryTxNoCpfp
- let recoveryTxFeeRateNoCpfp =
- FeeRatePerKw.FromFeeAndVSize(recoveryTxFeeNoCpfp, uint64 (recoveryTxNoCpfp.GetVirtualSize()))
- assert FeesHelper.FeeRatesApproxEqual recoveryTxFeeRateNoCpfp newFeeRate
- let combinedFeeRateNoCpfp =
- FeeRatePerKw.FromFeeAndVSize(
- recoveryTxFeeNoCpfp + forceCloseTxFee,
- uint64 (recoveryTxNoCpfp.GetVirtualSize() + forceCloseTx.GetVirtualSize())
- )
- assert (not <| FeesHelper.FeeRatesApproxEqual combinedFeeRateNoCpfp oldFeeRate)
- assert (not <| FeesHelper.FeeRatesApproxEqual combinedFeeRateNoCpfp newFeeRate)
-
- let! recoveryTxStringWithCpfpRes =
- (Node.Server serverWallet.NodeServer).CreateRecoveryTxForRemoteForceClose
- channelId
- wrappedForceCloseTx
- true
- let recoveryTxStringWithCpfp =
- UnwrapResult
- recoveryTxStringWithCpfpRes
- "force close failed to recover funds from the commitment tx"
- let recoveryTxWithCpfp = Transaction.Parse(recoveryTxStringWithCpfp.Tx.ToString(), Network.RegTest)
- let! recoveryTxFeeWithCpfp = FeesHelper.GetFeeFromTransaction recoveryTxWithCpfp
- let recoveryTxFeeRateWithCpfp =
- FeeRatePerKw.FromFeeAndVSize(recoveryTxFeeWithCpfp, uint64 (recoveryTxWithCpfp.GetVirtualSize()))
- assert (not <| FeesHelper.FeeRatesApproxEqual recoveryTxFeeRateWithCpfp oldFeeRate)
- assert (not <| FeesHelper.FeeRatesApproxEqual recoveryTxFeeRateWithCpfp newFeeRate)
- let combinedFeeRateWithCpfp =
- FeeRatePerKw.FromFeeAndVSize(
- recoveryTxFeeWithCpfp + forceCloseTxFee,
- uint64 (recoveryTxWithCpfp.GetVirtualSize() + forceCloseTx.GetVirtualSize())
- )
- assert FeesHelper.FeeRatesApproxEqual combinedFeeRateWithCpfp newFeeRate
-
(serverWallet :> IDisposable).Dispose()
}
diff --git a/src/GWallet.Backend.Tests.End2End/packages.config b/src/GWallet.Backend.Tests.End2End/packages.config
index 61ee70abc..8cfaa6326 100644
--- a/src/GWallet.Backend.Tests.End2End/packages.config
+++ b/src/GWallet.Backend.Tests.End2End/packages.config
@@ -7,7 +7,7 @@
-
+
diff --git a/src/GWallet.Backend.Tests.Unit/ChannelMarshalling.fs b/src/GWallet.Backend.Tests.Unit/ChannelMarshalling.fs
index 899a49e4b..841029cc9 100644
--- a/src/GWallet.Backend.Tests.Unit/ChannelMarshalling.fs
+++ b/src/GWallet.Backend.Tests.Unit/ChannelMarshalling.fs
@@ -130,6 +130,9 @@ type ChannelMarshalling () =
"024172921dc95fd529d4e5418e2d59c8037c1d7ff03c10d9c432bbefdff49bb741"
]
}
+ },
+ "type": {
+ "case": "StaticRemoteKey"
}
},
"remotePerCommitmentSecrets": [],
diff --git a/src/GWallet.Backend.Tests.Unit/GWallet.Backend.Tests.Unit.fsproj b/src/GWallet.Backend.Tests.Unit/GWallet.Backend.Tests.Unit.fsproj
index ba19fcdf3..fb06d86da 100644
--- a/src/GWallet.Backend.Tests.Unit/GWallet.Backend.Tests.Unit.fsproj
+++ b/src/GWallet.Backend.Tests.Unit/GWallet.Backend.Tests.Unit.fsproj
@@ -163,13 +163,13 @@
..\..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\DotNetLightning.Core.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\DotNetLightning.Core.dll
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\InternalBech32Encoder.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\InternalBech32Encoder.dll
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\ResultUtils.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\ResultUtils.dll
..\..\packages\Portable.BouncyCastle.1.8.10\lib\net40\BouncyCastle.Crypto.dll
diff --git a/src/GWallet.Backend.Tests.Unit/packages.config b/src/GWallet.Backend.Tests.Unit/packages.config
index adab0bcf6..dafb1b877 100644
--- a/src/GWallet.Backend.Tests.Unit/packages.config
+++ b/src/GWallet.Backend.Tests.Unit/packages.config
@@ -1,6 +1,6 @@
-
+
diff --git a/src/GWallet.Backend/GWallet.Backend.fsproj b/src/GWallet.Backend/GWallet.Backend.fsproj
index 67071ad3f..b7479f3a1 100644
--- a/src/GWallet.Backend/GWallet.Backend.fsproj
+++ b/src/GWallet.Backend/GWallet.Backend.fsproj
@@ -76,6 +76,7 @@
+
@@ -333,13 +334,13 @@
..\..\packages\HtmlAgilityPack.1.11.24\lib\Net45\HtmlAgilityPack.dll
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\DotNetLightning.Core.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\DotNetLightning.Core.dll
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\InternalBech32Encoder.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\InternalBech32Encoder.dll
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\ResultUtils.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\ResultUtils.dll
..\..\packages\FSharpx.Collections.3.0.1\lib\netstandard2.0\FSharpx.Collections.dll
diff --git a/src/GWallet.Backend/UtxoCoin/Lightning/ChainWatcher.fs b/src/GWallet.Backend/UtxoCoin/Lightning/ChainWatcher.fs
index 60463137e..e9737a389 100644
--- a/src/GWallet.Backend/UtxoCoin/Lightning/ChainWatcher.fs
+++ b/src/GWallet.Backend/UtxoCoin/Lightning/ChainWatcher.fs
@@ -82,7 +82,17 @@ module public ChainWatcher =
}
- return! ListAsyncTryPick historyList checkIfRevokedCommitment
+ return!
+ ListAsyncTryPick
+ historyList
+ (fun txInfo ->
+ // Only check spending txs with at least 1 conf, cause we need at least 1 conf to broadcast our
+ // penalty tx (because it tries to spend to_remote output as well, which is time-locked for one block)
+ if txInfo.Height > 0u then
+ checkIfRevokedCommitment txInfo
+ else
+ async { return None })
+
}
let CheckForChannelFraudsAndSendRevocationTx (accounts: seq)
diff --git a/src/GWallet.Backend/UtxoCoin/Lightning/ChannelManagement.fs b/src/GWallet.Backend/UtxoCoin/Lightning/ChannelManagement.fs
index f4239835b..d1eaab106 100644
--- a/src/GWallet.Backend/UtxoCoin/Lightning/ChannelManagement.fs
+++ b/src/GWallet.Backend/UtxoCoin/Lightning/ChannelManagement.fs
@@ -42,6 +42,7 @@ type LocallyForceClosedData =
Currency: Currency
ToSelfDelay: uint16
ForceCloseTxId: TransactionIdentifier
+ ClosingTimestampUtc: DateTime
}
member self.GetRemainingConfirmations (): Async =
async {
@@ -97,6 +98,7 @@ type ChannelInfo =
Currency = currency
ToSelfDelay = serializedChannel.SavedChannelState.StaticChannelConfig.LocalParams.ToSelfDelay.Value
ForceCloseTxId = forceCloseTxId
+ ClosingTimestampUtc = UnwrapOption serializedChannel.ClosingTimestampUtc "BUG: closing date is empty after local force close"
}
| None ->
if serializedChannel.NegotiatingState.HasEnteredShutdown() then
diff --git a/src/GWallet.Backend/UtxoCoin/Lightning/ForceCloseTransaction.fs b/src/GWallet.Backend/UtxoCoin/Lightning/ForceCloseTransaction.fs
index ca03746c7..ce6d0d039 100644
--- a/src/GWallet.Backend/UtxoCoin/Lightning/ForceCloseTransaction.fs
+++ b/src/GWallet.Backend/UtxoCoin/Lightning/ForceCloseTransaction.fs
@@ -40,7 +40,8 @@ module public ForceCloseTransaction =
(Commitments.RemoteCommitAmount
savedChannelState.StaticChannelConfig.IsFunder
savedChannelState.StaticChannelConfig.RemoteParams
- savedChannelState.RemoteCommit)
+ savedChannelState.RemoteCommit
+ savedChannelState.StaticChannelConfig.Type.CommitmentFormat)
.ToLocal
.ToDecimal(MoneyUnit.Satoshi)
@@ -48,7 +49,8 @@ module public ForceCloseTransaction =
(Commitments.RemoteCommitAmount
savedChannelState.StaticChannelConfig.IsFunder
savedChannelState.StaticChannelConfig.RemoteParams
- savedChannelState.RemoteCommit)
+ savedChannelState.RemoteCommit
+ savedChannelState.StaticChannelConfig.Type.CommitmentFormat)
.ToRemote
.ToDecimal(MoneyUnit.Satoshi)
diff --git a/src/GWallet.Backend/UtxoCoin/Lightning/Node.fs b/src/GWallet.Backend/UtxoCoin/Lightning/Node.fs
index 6ee5ceca2..a775b9b32 100644
--- a/src/GWallet.Backend/UtxoCoin/Lightning/Node.fs
+++ b/src/GWallet.Backend/UtxoCoin/Lightning/Node.fs
@@ -3,6 +3,7 @@ namespace GWallet.Backend.UtxoCoin.Lightning
open System
open System.IO
open System.Net
+open System.Linq
open NBitcoin
open DotNetLightning.Chain
@@ -17,17 +18,22 @@ open NOnion.Network
open GWallet.Backend
open GWallet.Backend.UtxoCoin
+open GWallet.Backend.UtxoCoin.Account
+open GWallet.Backend.UtxoCoin.Lightning.Validation
open GWallet.Backend.FSharpUtil
open GWallet.Backend.FSharpUtil.UwpHacks
type internal NodeOpenChannelError =
| Connect of ConnectError
+ | InitMsgValidation of InitMsgValidationError
| OpenChannel of OpenChannelError
interface IErrorMsg with
member self.Message =
match self with
| Connect connectError ->
SPrintF1 "error connecting: %s" (connectError :> IErrorMsg).Message
+ | InitMsgValidation validationError ->
+ SPrintF1 "invalid remote party initialization: %s" (validationError :> IErrorMsg).Message
| OpenChannel openChannelError ->
SPrintF1 "error opening channel: %s" (openChannelError :> IErrorMsg).Message
member __.ChannelBreakdown: bool =
@@ -35,12 +41,15 @@ type internal NodeOpenChannelError =
type internal NodeAcceptChannelError =
| AcceptPeer of ConnectError
+ | InitMsgValidation of InitMsgValidationError
| AcceptChannel of AcceptChannelError
interface IErrorMsg with
member self.Message =
match self with
| AcceptPeer connectError ->
SPrintF1 "error accepting connection: %s" (connectError :> IErrorMsg).Message
+ | InitMsgValidation validationError ->
+ SPrintF1 "invalid remote party initialization: %s" (validationError :> IErrorMsg).Message
| AcceptChannel acceptChannelError ->
SPrintF1 "error accepting channel: %s" (acceptChannelError :> IErrorMsg).Message
member __.ChannelBreakdown: bool =
@@ -254,25 +263,29 @@ type NodeClient internal (channelStore: ChannelStore, nodeMasterPrivKey: NodeMas
|> ignore
return Error <| (NodeOpenChannelError.Connect connectError :> IErrorMsg)
| Ok peerNode ->
- let! outgoingUnfundedChannelRes =
- OutgoingUnfundedChannel.OpenChannel
- peerNode
- self.Account
- channelCapacity
- match outgoingUnfundedChannelRes with
- | Error openChannelError ->
- if openChannelError.PossibleBug then
- let msg =
- SPrintF3
- "error opening channel with peer %s of capacity %M: %s"
- (nodeIdentifier.ToString())
- channelCapacity.ValueToSend
- (openChannelError :> IErrorMsg).Message
- Infrastructure.ReportWarningMessage msg
- |> ignore
- return Error <| (NodeOpenChannelError.OpenChannel openChannelError :> IErrorMsg)
- | Ok outgoingUnfundedChannel ->
- return Ok <| PendingChannel(outgoingUnfundedChannel)
+ match ValidateRemoteInitMsg peerNode.InitMsg with
+ | Error validationError ->
+ return Error <| (NodeOpenChannelError.InitMsgValidation validationError :> IErrorMsg)
+ | Ok _ ->
+ let! outgoingUnfundedChannelRes =
+ OutgoingUnfundedChannel.OpenChannel
+ peerNode
+ self.Account
+ channelCapacity
+ match outgoingUnfundedChannelRes with
+ | Error openChannelError ->
+ if openChannelError.PossibleBug then
+ let msg =
+ SPrintF3
+ "error opening channel with peer %s of capacity %M: %s"
+ (nodeIdentifier.ToString())
+ channelCapacity.ValueToSend
+ (openChannelError :> IErrorMsg).Message
+ Infrastructure.ReportWarningMessage msg
+ |> ignore
+ return Error (NodeOpenChannelError.OpenChannel openChannelError :> IErrorMsg)
+ | Ok outgoingUnfundedChannel ->
+ return Ok <| PendingChannel outgoingUnfundedChannel
}
member internal self.SendHtlcPayment (channelId: ChannelIdentifier)
@@ -423,23 +436,27 @@ type NodeServer internal (channelStore: ChannelStore, transportListener: Transpo
|> ignore
return Error <| (NodeAcceptChannelError.AcceptPeer connectError :> IErrorMsg)
| Ok peerNode ->
- let! fundedChannelRes = FundedChannel.AcceptChannel peerNode self.Account
- match fundedChannelRes with
- | Error acceptChannelError ->
- if acceptChannelError.PossibleBug then
- let msg =
- SPrintF2
- "error accepting channel from peer %s: %s"
- (peerNode.NodeEndPoint.ToString())
- (acceptChannelError :> IErrorMsg).Message
- Infrastructure.ReportWarningMessage msg
- |> ignore
- return Error <| (NodeAcceptChannelError.AcceptChannel acceptChannelError :> IErrorMsg)
- | Ok fundedChannel ->
- let channelId = fundedChannel.ChannelId
- let txId = fundedChannel.FundingTxId
- (fundedChannel :> IDisposable).Dispose()
- return Ok (channelId, txId)
+ match ValidateRemoteInitMsg peerNode.InitMsg with
+ | Error validationError ->
+ return Error (NodeOpenChannelError.InitMsgValidation validationError :> IErrorMsg)
+ | Ok _ ->
+ let! fundedChannelRes = FundedChannel.AcceptChannel peerNode self.Account
+ match fundedChannelRes with
+ | Error acceptChannelError ->
+ if acceptChannelError.PossibleBug then
+ let msg =
+ SPrintF2
+ "error accepting channel from peer %s: %s"
+ (peerNode.NodeEndPoint.ToString())
+ (acceptChannelError :> IErrorMsg).Message
+ Infrastructure.ReportWarningMessage msg
+ |> ignore
+ return Error <| (NodeAcceptChannelError.AcceptChannel acceptChannelError :> IErrorMsg)
+ | Ok fundedChannel ->
+ let channelId = fundedChannel.ChannelId
+ let txId = fundedChannel.FundingTxId
+ (fundedChannel :> IDisposable).Dispose()
+ return Ok (channelId, txId)
}
// use ReceiveLightningEvent instead
@@ -681,6 +698,127 @@ type Node =
| Server nodeServer -> nodeServer.Account
:> IAccount
+ member self.CreateAnchorFeeBumpForForceClose
+ (channelId: ChannelIdentifier)
+ (commitmentTxString: string)
+ (password: string)
+ : Async> =
+ async {
+ let account = self.Account
+ let privateKey = Account.GetPrivateKey (account :?> NormalUtxoAccount) password
+ let nodeMasterPrivKey =
+ match self with
+ | Client nodeClient -> nodeClient.NodeMasterPrivKey
+ | Server nodeServer -> nodeServer.NodeMasterPrivKey
+ let currency = self.Account.Currency
+ let serializedChannel = self.ChannelStore.LoadChannel channelId
+ let! feeBumpTransaction = async {
+ let network =
+ UtxoCoin.Account.GetNetwork currency
+ let commitmentTx =
+ Transaction.Parse(commitmentTxString, network)
+ let channelPrivKeys =
+ let channelIndex = serializedChannel.ChannelIndex
+ nodeMasterPrivKey.ChannelPrivKeys channelIndex
+ let targetAddress =
+ let originAddress = self.Account.PublicAddress
+ BitcoinAddress.Create(originAddress, network)
+ let! feeRate = async {
+ let! feeEstimator = FeeEstimator.Create currency
+ return feeEstimator.FeeRatePerKw
+ }
+ let transactionBuilderResult =
+ ForceCloseFundsRecovery.tryClaimAnchorFromCommitmentTx
+ channelPrivKeys
+ serializedChannel.SavedChannelState.StaticChannelConfig
+ commitmentTx
+ match transactionBuilderResult with
+ | Error (LocalAnchorRecoveryError.InvalidCommitmentTx invalidCommitmentTxError) ->
+ return failwith ("invalid commitment tx for force-closing: " + invalidCommitmentTxError.Message)
+ | Error LocalAnchorRecoveryError.AnchorNotFound ->
+ self.ChannelStore.ArchiveChannel channelId
+ return Error <| ClosingBalanceBelowDustLimit
+ | Ok transactionBuilder ->
+ let job =
+ Account.GetElectrumScriptHashFromPublicAddress account.Currency account.PublicAddress
+ |> ElectrumClient.GetUnspentTransactionOutputs
+ let! utxos = Server.Query account.Currency (QuerySettings.Default ServerSelectionMode.Fast) job None
+
+ if not (utxos.Any()) then
+ return raise InsufficientFunds
+ let possibleInputs =
+ seq {
+ for utxo in utxos do
+ yield {
+ TransactionId = utxo.TxHash
+ OutputIndex = utxo.TxPos
+ Value = utxo.Value
+ }
+ }
+
+ // first ones are the smallest ones
+ let inputsOrderedByAmount = possibleInputs.OrderBy(fun utxo -> utxo.Value) |> List.ofSeq
+
+ transactionBuilder.AddKeys privateKey |> ignore
+ transactionBuilder.SendAllRemaining targetAddress |> ignore
+
+ let requiredParentTxFee = feeRate.AsNBitcoinFeeRate().GetFee commitmentTx
+ let actualParentTxFee =
+ serializedChannel.FundingScriptCoin() :> ICoin
+ |> Array.singleton
+ |> commitmentTx.GetFee
+ let deltaParentTxFee = requiredParentTxFee - actualParentTxFee
+
+ let rec addUtxoForChildFee unusedUtxos =
+ async {
+ try
+ let fees = transactionBuilder.EstimateFees (feeRate.AsNBitcoinFeeRate())
+ return fees, unusedUtxos
+ with
+ | :? NBitcoin.NotEnoughFundsException as _ex ->
+ match unusedUtxos with
+ | [] -> return raise InsufficientFunds
+ | head::tail ->
+ let! newInput = head |> ConvertToInputOutpointInfo account.Currency
+ let newCoin = newInput |> ConvertToICoin (account :?> IUtxoAccount)
+ transactionBuilder.AddCoin newCoin |> ignore
+ return! addUtxoForChildFee tail
+ }
+
+ let rec addUtxosForParentFeeAndFinalize unusedUtxos =
+ async {
+ try
+ return transactionBuilder.BuildTransaction true
+ with
+ | :? NBitcoin.NotEnoughFundsException as _ex ->
+ match unusedUtxos with
+ | [] -> return raise InsufficientFunds
+ | head::tail ->
+ let! newInput = head |> ConvertToInputOutpointInfo account.Currency
+ let newCoin = newInput |> ConvertToICoin (account :?> IUtxoAccount)
+ transactionBuilder.AddCoin newCoin |> ignore
+ return! addUtxosForParentFeeAndFinalize tail
+ }
+
+ let! childFee, unusedUtxos = addUtxoForChildFee inputsOrderedByAmount
+ transactionBuilder.SendFees (childFee + deltaParentTxFee) |> ignore
+ let! transaction = addUtxosForParentFeeAndFinalize unusedUtxos
+
+ let feeBumpTransaction: FeeBumpTx =
+ {
+ ChannelId = channelId
+ Currency = currency
+ Fee = MinerFee ((childFee + deltaParentTxFee).Satoshi, DateTime.UtcNow, currency)
+ Tx =
+ {
+ NBitcoinTx = transaction
+ }
+ }
+ return Ok feeBumpTransaction
+ }
+ return feeBumpTransaction
+ }
+
member self.CreateRecoveryTxForLocalForceClose
(channelId: ChannelIdentifier)
(commitmentTxString: string)
@@ -755,6 +893,7 @@ type Node =
ForceCloseTxIdOpt =
TransactionIdentifier.Parse forceCloseTxId
|> Some
+ ClosingTimestampUtc = Some DateTime.UtcNow
}
self.ChannelStore.SaveChannel newSerializedChannel
return Ok forceCloseTxId
@@ -763,7 +902,6 @@ type Node =
member self.CreateRecoveryTxForRemoteForceClose
(channelId: ChannelIdentifier)
(closingTx: ForceCloseTx)
- (requiresCpfp: bool)
: Async> =
async {
let nodeMasterPrivKey =
@@ -799,15 +937,7 @@ type Node =
return Error <| ClosingBalanceBelowDustLimit
| Ok transactionBuilder ->
transactionBuilder.SendAll targetAddress |> ignore
- let fee =
- if requiresCpfp then
- FeeEstimator.EstimateCpfpFee
- transactionBuilder
- feeRate
- closingTx.Tx.NBitcoinTx
- (serializedChannel.FundingScriptCoin())
- else
- transactionBuilder.EstimateFees (feeRate.AsNBitcoinFeeRate())
+ let fee = transactionBuilder.EstimateFees (feeRate.AsNBitcoinFeeRate())
transactionBuilder.SendFees fee |> ignore
let recoveryTransaction: RecoveryTx =
{
diff --git a/src/GWallet.Backend/UtxoCoin/Lightning/Settings.fs b/src/GWallet.Backend/UtxoCoin/Lightning/Settings.fs
index 23bfbc89d..a84a0b1e4 100644
--- a/src/GWallet.Backend/UtxoCoin/Lightning/Settings.fs
+++ b/src/GWallet.Backend/UtxoCoin/Lightning/Settings.fs
@@ -5,6 +5,7 @@ open System
open NBitcoin
open DotNetLightning.Utils
open DotNetLightning.Serialization
+open DotNetLightning.Serialization.Msgs
open DotNetLightning.Chain
open DotNetLightning.Channel
@@ -57,6 +58,8 @@ module Settings =
let internal SupportedFeatures (currency: Currency) (fundingOpt: Option) =
let featureBits = FeatureBits.Zero
featureBits.SetFeature Feature.OptionDataLossProtect FeaturesSupport.Optional true
+ featureBits.SetFeature Feature.OptionStaticRemoteKey FeaturesSupport.Mandatory true
+ featureBits.SetFeature Feature.OptionAnchorZeroFeeHtlcTx FeaturesSupport.Mandatory true
if currency = Currency.LTC then
let featureType =
match fundingOpt with
diff --git a/src/GWallet.Backend/UtxoCoin/Lightning/Transactions.fs b/src/GWallet.Backend/UtxoCoin/Lightning/Transactions.fs
index 88597f24b..cfd01b5fe 100644
--- a/src/GWallet.Backend/UtxoCoin/Lightning/Transactions.fs
+++ b/src/GWallet.Backend/UtxoCoin/Lightning/Transactions.fs
@@ -25,6 +25,14 @@ type MutualCloseCpfp =
Fee: MinerFee
}
+type FeeBumpTx =
+ {
+ ChannelId: ChannelIdentifier
+ Currency: Currency
+ Tx: UtxoTransaction
+ Fee: MinerFee
+ }
+
type RecoveryTx =
{
ChannelId: ChannelIdentifier
diff --git a/src/GWallet.Backend/UtxoCoin/Lightning/Validation.fs b/src/GWallet.Backend/UtxoCoin/Lightning/Validation.fs
new file mode 100644
index 000000000..61201901d
--- /dev/null
+++ b/src/GWallet.Backend/UtxoCoin/Lightning/Validation.fs
@@ -0,0 +1,27 @@
+namespace GWallet.Backend.UtxoCoin.Lightning
+
+open DotNetLightning.Serialization
+open DotNetLightning.Serialization.Msgs
+open ResultUtils.Portability
+
+open GWallet.Backend.FSharpUtil
+
+module Validation =
+ type internal InitMsgValidationError =
+ | NoAnchorSupport
+ interface IErrorMsg with
+ member self.Message =
+ match self with
+ | NoAnchorSupport ->
+ "no anchor channel support (CPFP not possible, so potential loss of funds with this node; rather open a channel with a better node)"
+ member __.ChannelBreakdown: bool =
+ false
+
+ let internal ValidateRemoteInitMsg (remoteInit: InitMsg): Result =
+ let hasAmchorSupport =
+ remoteInit.Features.HasFeature Feature.OptionAnchorZeroFeeHtlcTx
+
+ if not hasAmchorSupport then
+ Error InitMsgValidationError.NoAnchorSupport
+ else
+ Ok ()
diff --git a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs
index c6ca9f7ab..8754c90ec 100644
--- a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs
+++ b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs
@@ -172,7 +172,7 @@ module Account =
return None
}
- let private ConvertToICoin (account: IUtxoAccount) (inputOutpointInfo: TransactionInputOutpointInfo): ICoin =
+ let internal ConvertToICoin (account: IUtxoAccount) (inputOutpointInfo: TransactionInputOutpointInfo): ICoin =
let txHash = uint256 inputOutpointInfo.TransactionHash
let scriptPubKeyInBytes = NBitcoin.DataEncoders.Encoders.Hex.DecodeData inputOutpointInfo.DestinationInHex
let scriptPubKey = Script(scriptPubKeyInBytes)
@@ -216,7 +216,7 @@ module Account =
Value: Int64;
}
- let private ConvertToInputOutpointInfo currency (utxo: UnspentTransactionOutputInfo)
+ let internal ConvertToInputOutpointInfo currency (utxo: UnspentTransactionOutputInfo)
: Async =
async {
let job = ElectrumClient.GetBlockchainTransaction utxo.TransactionId
diff --git a/src/GWallet.Backend/packages.config b/src/GWallet.Backend/packages.config
index 965c7d1fd..6d3123ab2 100644
--- a/src/GWallet.Backend/packages.config
+++ b/src/GWallet.Backend/packages.config
@@ -1,7 +1,7 @@
-
+
diff --git a/src/GWallet.Frontend.Console/GWallet.Frontend.Console.fsproj b/src/GWallet.Frontend.Console/GWallet.Frontend.Console.fsproj
index 83a3848ff..5d92be4db 100644
--- a/src/GWallet.Frontend.Console/GWallet.Frontend.Console.fsproj
+++ b/src/GWallet.Frontend.Console/GWallet.Frontend.Console.fsproj
@@ -96,7 +96,7 @@
- ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220204-0457-git-9c7a03c\lib\netstandard2.0\ResultUtils.dll
+ ..\..\packages\DotNetLightning.Kiss.1.1.2-date20220322-1031-git-5379324\lib\netstandard2.0\ResultUtils.dll
diff --git a/src/GWallet.Frontend.Console/LayerTwo.fs b/src/GWallet.Frontend.Console/LayerTwo.fs
index 84e256de2..42c2feeba 100644
--- a/src/GWallet.Frontend.Console/LayerTwo.fs
+++ b/src/GWallet.Frontend.Console/LayerTwo.fs
@@ -654,6 +654,8 @@ module LayerTwo =
| None -> ()
}
+ let TimeBeforeCpfpSuggestion = TimeSpan.FromMinutes 15.
+
let private txRecoveryMsg = "A transaction must be sent to recover funds."
let ClaimFundsIfTimelockExpired
@@ -692,13 +694,74 @@ module LayerTwo =
trySendRecoveryTx
|> UserInteraction.TryWithPasswordAsync
else
- return seq {
- yield! UserInteraction.DisplayLightningChannelStatus channelInfo
- yield sprintf " channel force-closed"
- yield sprintf
- " waiting for %i more confirmations before funds are recovered"
- remainingConfirmations
- }
+ // only check for CPFP if there is 0 confirmations
+ if remainingConfirmations = locallyForceClosedData.ToSelfDelay then
+ let! isCpfpNeeded =
+ ChannelManager.IsCpfpNeededForFundingSpendingTx
+ channelStore
+ channelInfo.ChannelId
+ locallyForceClosedData.ForceCloseTxId
+ if isCpfpNeeded && locallyForceClosedData.ClosingTimestampUtc.Add(TimeBeforeCpfpSuggestion) < DateTime.UtcNow then
+ let msg =
+ sprintf
+ "Channel %s has been force-closed but the closure transaction didn't confirm yet. Do you want to increase the fee (via creation of child transaction, e.g. CPFP)?"
+ (ChannelId.ToString channelInfo.ChannelId)
+ if UserInteraction.AskYesNo msg then
+ let trySendAnchorCpfp (password: string) =
+ async {
+ let nodeClient = Lightning.Connection.StartClient channelStore password
+ let commitmentTx = channelStore.GetCommitmentTx channelInfo.ChannelId
+ try
+ let! feeBumpTxRes = (Node.Client nodeClient).CreateAnchorFeeBumpForForceClose channelInfo.ChannelId commitmentTx password
+ let feeBumpTx = UnwrapResult feeBumpTxRes "shouldn't happen because we don't force close a channel if our output is under the dust limit"
+ if UserInteraction.ConfirmTxFee feeBumpTx.Fee then
+ do! UtxoCoin.Account.BroadcastRawTransaction
+ feeBumpTx.Currency
+ (feeBumpTx.Tx.ToString())
+ |> Async.Ignore
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield sprintf " channel force-closed"
+ yield sprintf " CPFP performed, waiting for %i more confirmations before funds are recovered" remainingConfirmations
+ }
+ else
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield sprintf " channel force-closed"
+ yield sprintf " waiting for %i more confirmations before funds are recovered" remainingConfirmations
+ }
+ with
+ | :? InsufficientFunds ->
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield sprintf " channel force-closed"
+ yield sprintf " CPFP failed due to insufficient funds in your wallet"
+ yield sprintf " waiting for %i more confirmations before funds are recovered" remainingConfirmations
+ }
+ }
+ return!
+ trySendAnchorCpfp
+ |> UserInteraction.TryWithPasswordAsync
+ else
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield sprintf " channel force-closed"
+ yield sprintf " waiting for %i more confirmations before funds are recovered" remainingConfirmations
+ }
+ else
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield sprintf " channel force-closed"
+ yield sprintf " waiting for %i more confirmations before funds are recovered" remainingConfirmations
+ }
+ else
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield sprintf " channel force-closed"
+ yield sprintf
+ " waiting for %i more confirmations before funds are recovered"
+ remainingConfirmations
+ }
}
let FindForceClose
@@ -723,23 +786,79 @@ module LayerTwo =
(closingTxHeightOpt: Option)
: Async> =
async {
- Console.WriteLine(sprintf "Channel %s has been force-closed by the counterparty." (ChannelId.ToString channelInfo.ChannelId))
- Console.WriteLine txRecoveryMsg
- let tryClaimFunds password =
- async {
- let nodeClient = Lightning.Connection.StartClient channelStore password
- let sendTx =
- if closingTxHeightOpt.IsNone then
- Console.WriteLine "The remote party tried to perform a forced closing of the channel (probably because it couldn't contact your node) recently (or not so recently if your device has been offline for a while), but their transaction didn't confirm yet."
- UserInteraction.AskYesNo "Do you want to send an extra transaction (increasing the fee) that helps the channel to get closed faster? If you don't, your funds in the channel will not be recovered yet."
- else
- true
- if sendTx then
+ if closingTxHeightOpt.IsNone then
+ let! isCpfpNeeded =
+ ChannelManager.IsCpfpNeededForFundingSpendingTx
+ channelStore
+ channelInfo.ChannelId
+ closingTx.Tx.Id
+ // we can't check for ``LayerTwo.TimeBeforeCPFPSuggestion`` TimeSpan here because we don't know when remote party broadcasted their force close tx
+ if isCpfpNeeded then
+ if UserInteraction.AskYesNo "You can speed up confirmation by sending a new (child) transaction that would increase the overall fees (CPFP), do you wish to proceed?" then
+ let trySendAnchorCpfp (password: string) =
+ async {
+ let nodeClient = Lightning.Connection.StartClient channelStore password
+ let commitmentTx = channelStore.GetCommitmentTx channelInfo.ChannelId
+ try
+ let! feeBumpTxRes = (Node.Client nodeClient).CreateAnchorFeeBumpForForceClose channelInfo.ChannelId commitmentTx password
+ match feeBumpTxRes with
+ | Ok feeBumpTx ->
+ if UserInteraction.ConfirmTxFee feeBumpTx.Fee then
+ do! UtxoCoin.Account.BroadcastRawTransaction
+ feeBumpTx.Currency
+ (feeBumpTx.Tx.ToString())
+ |> Async.Ignore
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield " channel closed by counterparty"
+ yield " CPFP performed, waiting for 1 confirmation before funds are recovered"
+ }
+ else
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield " channel closed by counterparty"
+ yield " waiting for 1 confirmation before funds are recovered"
+ }
+ | Error ClosingBalanceBelowDustLimit ->
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield " channel closed by counterparty"
+ yield " Local channel balance was too small (below the \"dust\" limit) so no CPFP were performed."
+ }
+ with
+ | :? InsufficientFunds ->
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield " channel force-closed"
+ yield " CPFP failed due to insufficient funds in your wallet"
+ yield " waiting for 1 confirmation before funds are recovered"
+ }
+ }
+ return!
+ trySendAnchorCpfp
+ |> UserInteraction.TryWithPasswordAsync
+ else
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield " channel closed by counterparty"
+ yield " wait for 1 confirmation to recover your funds"
+ }
+ else
+ return seq {
+ yield! UserInteraction.DisplayLightningChannelStatus channelInfo
+ yield " channel closed by counterparty"
+ yield " wait for 1 confirmation to recover your funds"
+ }
+ else
+ Console.WriteLine(sprintf "Channel %s has been force-closed by the counterparty." (ChannelId.ToString channelInfo.ChannelId))
+ Console.WriteLine txRecoveryMsg
+ let tryClaimFunds password =
+ async {
+ let nodeClient = Lightning.Connection.StartClient channelStore password
let! recoveryTxResult =
(Node.Client nodeClient).CreateRecoveryTxForRemoteForceClose
channelInfo.ChannelId
closingTx
- closingTxHeightOpt.IsNone
match recoveryTxResult with
| Ok recoveryTx ->
if UserInteraction.ConfirmTxFee recoveryTx.Fee then
@@ -764,19 +883,11 @@ module LayerTwo =
yield " channel closed by counterparty"
yield " Local channel balance was too small (below the \"dust\" limit) so no funds were recovered."
}
- else
- return seq {
- yield! UserInteraction.DisplayLightningChannelStatus channelInfo
- yield " channel closed by counterparty"
- yield " remote party's force-close transaction confirmation in progress"
- }
- }
+ }
- return! UserInteraction.TryWithPasswordAsync tryClaimFunds
+ return! UserInteraction.TryWithPasswordAsync tryClaimFunds
}
- let TimeBeforeCpfpSuggestion = TimeSpan.FromMinutes 15.
-
let HandleMutualClose
(channelStore: ChannelStore)
(channelInfo: ChannelInfo)
@@ -801,7 +912,7 @@ module LayerTwo =
if isCpfpNeeded && closingTimestampUtc.Add(TimeBeforeCpfpSuggestion) < DateTime.UtcNow then
let msg =
sprintf
- "Channel %s has been mutually-closed but the closure transaction didn't confirm yet. Do you want to increase the fee (via CPFP)?"
+ "Channel %s has been mutually-closed but the closure transaction didn't confirm yet. Do you want to increase the fee (via creation of child transaction, e.g. CPFP)?"
(ChannelId.ToString channelInfo.ChannelId)
let doCpfp = UserInteraction.AskYesNo msg
if doCpfp then