From 02b80aac266813d0c86b3fa442892a1693c31156 Mon Sep 17 00:00:00 2001 From: Afshin Arani Date: Thu, 13 Apr 2023 16:32:28 +0330 Subject: [PATCH] Backend/Utxo: support native segwit Previously, native segwit was not widly supported so it was necessary to do segwit over P2SH, these days native segwit is supported by most wallets and with it's lower fee is the recommended choice. Lightning protocol is even dropping support for using P2SH shutdown scripts [1]. This commit adds support for native segwit (P2WPKH) while keeping the support for spending funds in users's old P2SH wallets. [1] https://github.com/lightning/bolts/commit/8f2104e3b6829b7c8ed190359326ac93eedc9ff5 --- .../ElectrumIntegrationTests.fs | 4 +- src/GWallet.Backend/Config.fs | 2 + src/GWallet.Backend/ServerManager.fs | 2 +- .../UtxoCoin/ElectrumClient.fs | 29 +++++++- .../UtxoCoin/UtxoCoinAccount.fs | 66 ++++++++++++++++--- 5 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/GWallet.Backend.Tests/ElectrumIntegrationTests.fs b/src/GWallet.Backend.Tests/ElectrumIntegrationTests.fs index 696f2a5b5..a8f63aa65 100644 --- a/src/GWallet.Backend.Tests/ElectrumIntegrationTests.fs +++ b/src/GWallet.Backend.Tests/ElectrumIntegrationTests.fs @@ -176,14 +176,14 @@ type ElectrumIntegrationTests() = let currency = Currency.BTC let argument = GetScriptHash currency CheckElectrumServersConnection ElectrumServerSeedList.DefaultBtcList currency - (ElectrumClient.GetBalance argument) BalanceAssertion + (ElectrumClient.GetBalances (List.singleton argument)) BalanceAssertion [] member __.``can connect (just check balance) to some electrum LTC servers``() = let currency = Currency.LTC let argument = GetScriptHash currency CheckElectrumServersConnection ElectrumServerSeedList.DefaultLtcList currency - (ElectrumClient.GetBalance argument) BalanceAssertion + (ElectrumClient.GetBalances (List.singleton argument)) BalanceAssertion [] member __.``can get list UTXOs of an address from some electrum BTC servers``() = diff --git a/src/GWallet.Backend/Config.fs b/src/GWallet.Backend/Config.fs index 476f63c99..50668dcc5 100644 --- a/src/GWallet.Backend/Config.fs +++ b/src/GWallet.Backend/Config.fs @@ -34,6 +34,8 @@ module Config = // balances, so you might find discrepancies (e.g. the donut-chart-view) let internal NoNetworkBalanceForDebuggingPurposes = false + let internal UseNativeSegwit = false + let IsWindowsPlatform() = RuntimeInformation.IsOSPlatform OSPlatform.Windows diff --git a/src/GWallet.Backend/ServerManager.fs b/src/GWallet.Backend/ServerManager.fs index dbe42a612..958ff3d6e 100644 --- a/src/GWallet.Backend/ServerManager.fs +++ b/src/GWallet.Backend/ServerManager.fs @@ -117,7 +117,7 @@ module ServerManager = failwith <| SPrintF1 "Currency %A not UTXO?" currency let utxoFunc electrumServer = async { - let! bal = UtxoCoin.ElectrumClient.GetBalance scriptHash electrumServer + let! bal = UtxoCoin.ElectrumClient.GetBalances (List.singleton scriptHash) electrumServer return bal.Confirmed |> decimal } UtxoCoin.Server.GetServerFuncs utxoFunc servers |> Some diff --git a/src/GWallet.Backend/UtxoCoin/ElectrumClient.fs b/src/GWallet.Backend/UtxoCoin/ElectrumClient.fs index 594725962..568d5947e 100644 --- a/src/GWallet.Backend/UtxoCoin/ElectrumClient.fs +++ b/src/GWallet.Backend/UtxoCoin/ElectrumClient.fs @@ -51,7 +51,7 @@ module ElectrumClient = | { Encrypted = false; Protocol = Tcp port } -> Init electrumServer.ServerInfo.NetworkPath port - let GetBalance (scriptHash: string) (stratumServer: Async) = async { + let GetBalances (scriptHashes: List) (stratumServer: Async) = async { // FIXME: we should rather implement this method in terms of: // - querying all unspent transaction outputs (X) -> block heights included // - querying transaction history (Y) -> block heights included @@ -67,8 +67,31 @@ module ElectrumClient = // [ see https://www.youtube.com/watch?v=hjYCXOyDy7Y&feature=youtu.be&t=1171 for more information ] // * -> although that would be fixing only half of the problem, we also need proof of completeness let! stratumClient = stratumServer - let! balanceResult = stratumClient.BlockchainScriptHashGetBalance scriptHash - return balanceResult.Result + let rec innerGetBalances (scriptHashes: List) (result: BlockchainScriptHashGetBalanceInnerResult) = + async { + match scriptHashes with + | scriptHash::otherScriptHashes -> + let! balanceHash = stratumClient.BlockchainScriptHashGetBalance scriptHash + + return! + innerGetBalances + otherScriptHashes + { + result with + Unconfirmed = result.Unconfirmed + balanceHash.Result.Unconfirmed + Confirmed = result.Confirmed + balanceHash.Result.Confirmed + } + | [] -> + return result + } + + return! + innerGetBalances + scriptHashes + { + Unconfirmed = 0L + Confirmed = 0L + } } let GetUnspentTransactionOutputs scriptHash (stratumServer: Async) = async { diff --git a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs index 56cf8a76a..5001d5512 100644 --- a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs +++ b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs @@ -72,12 +72,23 @@ module Account = // TODO: measure how long does it take to get the script hash and if it's too long, cache it at app startup? BitcoinAddress.Create(publicAddress, GetNetwork currency) |> GetElectrumScriptHashFromAddress - let internal GetPublicAddressFromPublicKey currency (publicKey: PubKey) = + let internal GetSegwitP2shPublicAddressFromPublicKey currency (publicKey: PubKey) = + publicKey + .GetScriptPubKey(ScriptPubKeyType.SegwitP2SH) + .GetDestinationAddress(GetNetwork currency) + .ToString() + + let internal GetNativeSegwitPublicAddressFromPublicKey currency (publicKey: PubKey) = publicKey .GetScriptPubKey(ScriptPubKeyType.Segwit) - .Hash - .GetAddress(GetNetwork currency) + .GetDestinationAddress(GetNetwork currency) .ToString() + + let internal GetPublicAddressFromPublicKey = + if Config.UseNativeSegwit then + GetNativeSegwitPublicAddressFromPublicKey + else + GetSegwitP2shPublicAddressFromPublicKey let internal GetPublicAddressFromNormalAccountFile (currency: Currency) (accountFile: FileRepresentation): string = let pubKey = PubKey(accountFile.Name) @@ -139,11 +150,17 @@ module Account = (mode: ServerSelectionMode) (cancelSourceOption: Option) : Async = - let scriptHashHex = GetElectrumScriptHashFromPublicAddress account.Currency account.PublicAddress + let scriptHashesHex = + [ + GetNativeSegwitPublicAddressFromPublicKey account.Currency account.PublicKey + |> GetElectrumScriptHashFromPublicAddress account.Currency + GetSegwitP2shPublicAddressFromPublicKey account.Currency account.PublicKey + |> GetElectrumScriptHashFromPublicAddress account.Currency + ] let querySettings = QuerySettings.Balance(mode,(BalanceMatchWithCacheOrInitialBalance account.PublicAddress account.Currency)) - let balanceJob = ElectrumClient.GetBalance scriptHashHex + let balanceJob = ElectrumClient.GetBalances scriptHashesHex Server.Query account.Currency querySettings balanceJob cancelSourceOption let private GetBalancesFromServer (account: IUtxoAccount) @@ -176,9 +193,21 @@ module Account = let txHash = uint256 inputOutpointInfo.TransactionHash let scriptPubKeyInBytes = NBitcoin.DataEncoders.Encoders.Hex.DecodeData inputOutpointInfo.DestinationInHex let scriptPubKey = Script(scriptPubKeyInBytes) + // We convert the scriptPubKey to address temporarily to compare it with + // our own addresses, we could compare scriptPubKeys directly but we would + // need functions that return scriptPubKey of our addresses instead of a + // string. + let sourceAddress = scriptPubKey.GetDestinationAddress(GetNetwork account.Currency).ToString() let coin = Coin(txHash, uint32 inputOutpointInfo.OutputIndex, Money(inputOutpointInfo.ValueInSatoshis), scriptPubKey) - coin.ToScriptCoin account.PublicKey.WitHash.ScriptPubKey :> ICoin + if sourceAddress = GetSegwitP2shPublicAddressFromPublicKey account.Currency account.PublicKey then + coin.ToScriptCoin(account.PublicKey.WitHash.ScriptPubKey) :> ICoin + elif sourceAddress = GetNativeSegwitPublicAddressFromPublicKey account.Currency account.PublicKey then + coin :> ICoin + else + //We filter utxos based on scriptPubKey when retrieving from electrum + //so this is unreachable. + failwith "Unreachable: unrecognized scriptPubKey" let private CreateTransactionAndCoinsToBeSigned (account: IUtxoAccount) (transactionInputs: List) @@ -294,9 +323,28 @@ module Account = else newAcc,tail - let job = GetElectrumScriptHashFromPublicAddress account.Currency account.PublicAddress - |> ElectrumClient.GetUnspentTransactionOutputs - let! utxos = Server.Query account.Currency (QuerySettings.Default ServerSelectionMode.Fast) job None + let currency = account.Currency + + let getUtxos (publicAddress: string) = + async { + let job = GetElectrumScriptHashFromPublicAddress currency publicAddress + |> ElectrumClient.GetUnspentTransactionOutputs + + return! Server.Query currency (QuerySettings.Default ServerSelectionMode.Fast) job None + } + + let! utxos = + async { + let! nativeSegwitUtxos = + GetNativeSegwitPublicAddressFromPublicKey currency account.PublicKey + |> getUtxos + + let! legacySegwitUtxos = + GetSegwitP2shPublicAddressFromPublicKey currency account.PublicKey + |> getUtxos + + return Seq.concat [ nativeSegwitUtxos; legacySegwitUtxos ] + } if not (utxos.Any()) then failwith "No UTXOs found!"