From 8e198124a5381fcf44aeee25cdfe2c15e3d16885 Mon Sep 17 00:00:00 2001 From: Afshin Arani Date: Thu, 13 Apr 2023 16:32:28 +0330 Subject: [PATCH] Backend: migrate to 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 --- .../UtxoCoin/ElectrumClient.fs | 29 ++++++++- .../UtxoCoin/UtxoCoinAccount.fs | 63 ++++++++++++++++--- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/GWallet.Backend/UtxoCoin/ElectrumClient.fs b/src/GWallet.Backend/UtxoCoin/ElectrumClient.fs index 14bca3c32..9bef434a8 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 ec56c1a2c..eaed4a503 100644 --- a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs +++ b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs @@ -71,12 +71,20 @@ 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 = + GetNativeSegwitPublicAddressFromPublicKey let internal GetPublicAddressFromNormalAccountFile (currency: Currency) (accountFile: FileRepresentation): string = let pubKey = PubKey(accountFile.Name) @@ -137,11 +145,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) @@ -174,9 +188,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) @@ -292,9 +318,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 Array.concat [ nativeSegwitUtxos; legacySegwitUtxos ] + } if not (utxos.Any()) then failwith "No UTXOs found!"