Skip to content

Commit 7754684

Browse files
committed
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] lightning/bolts@8f2104e
1 parent 0d70e27 commit 7754684

File tree

5 files changed

+88
-15
lines changed

5 files changed

+88
-15
lines changed

src/GWallet.Backend.Tests/ElectrumIntegrationTests.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,14 @@ type ElectrumIntegrationTests() =
176176
let currency = Currency.BTC
177177
let argument = GetScriptHash currency
178178
CheckElectrumServersConnection ElectrumServerSeedList.DefaultBtcList currency
179-
(ElectrumClient.GetBalance argument) BalanceAssertion
179+
(ElectrumClient.GetBalances (List.singleton argument)) BalanceAssertion
180180

181181
[<Test>]
182182
member __.``can connect (just check balance) to some electrum LTC servers``() =
183183
let currency = Currency.LTC
184184
let argument = GetScriptHash currency
185185
CheckElectrumServersConnection ElectrumServerSeedList.DefaultLtcList currency
186-
(ElectrumClient.GetBalance argument) BalanceAssertion
186+
(ElectrumClient.GetBalances (List.singleton argument)) BalanceAssertion
187187

188188
[<Test>]
189189
member __.``can get list UTXOs of an address from some electrum BTC servers``() =

src/GWallet.Backend/Config.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ module Config =
3434
// balances, so you might find discrepancies (e.g. the donut-chart-view)
3535
let internal NoNetworkBalanceForDebuggingPurposes = false
3636

37+
let internal UseNativeSegwit = false
38+
3739
let IsWindowsPlatform() =
3840
RuntimeInformation.IsOSPlatform OSPlatform.Windows
3941

src/GWallet.Backend/ServerManager.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ module ServerManager =
117117
failwith <| SPrintF1 "Currency %A not UTXO?" currency
118118
let utxoFunc electrumServer =
119119
async {
120-
let! bal = UtxoCoin.ElectrumClient.GetBalance scriptHash electrumServer
120+
let! bal = UtxoCoin.ElectrumClient.GetBalances (List.singleton scriptHash) electrumServer
121121
return bal.Confirmed |> decimal
122122
}
123123
UtxoCoin.Server.GetServerFuncs utxoFunc servers |> Some

src/GWallet.Backend/UtxoCoin/ElectrumClient.fs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ module ElectrumClient =
5151
| { Encrypted = false; Protocol = Tcp port } ->
5252
Init electrumServer.ServerInfo.NetworkPath port
5353

54-
let GetBalance (scriptHash: string) (stratumServer: Async<StratumClient>) = async {
54+
let GetBalances (scriptHashes: List<string>) (stratumServer: Async<StratumClient>) = async {
5555
// FIXME: we should rather implement this method in terms of:
5656
// - querying all unspent transaction outputs (X) -> block heights included
5757
// - querying transaction history (Y) -> block heights included
@@ -67,8 +67,31 @@ module ElectrumClient =
6767
// [ see https://www.youtube.com/watch?v=hjYCXOyDy7Y&feature=youtu.be&t=1171 for more information ]
6868
// * -> although that would be fixing only half of the problem, we also need proof of completeness
6969
let! stratumClient = stratumServer
70-
let! balanceResult = stratumClient.BlockchainScriptHashGetBalance scriptHash
71-
return balanceResult.Result
70+
let rec innerGetBalances (scriptHashes: List<string>) (result: BlockchainScriptHashGetBalanceInnerResult) =
71+
async {
72+
match scriptHashes with
73+
| scriptHash::otherScriptHashes ->
74+
let! balanceHash = stratumClient.BlockchainScriptHashGetBalance scriptHash
75+
76+
return!
77+
innerGetBalances
78+
otherScriptHashes
79+
{
80+
result with
81+
Unconfirmed = result.Unconfirmed + balanceHash.Result.Unconfirmed
82+
Confirmed = result.Confirmed + balanceHash.Result.Confirmed
83+
}
84+
| [] ->
85+
return result
86+
}
87+
88+
return!
89+
innerGetBalances
90+
scriptHashes
91+
{
92+
Unconfirmed = 0L
93+
Confirmed = 0L
94+
}
7295
}
7396

7497
let GetUnspentTransactionOutputs scriptHash (stratumServer: Async<StratumClient>) = async {

src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,23 @@ module Account =
7272
// TODO: measure how long does it take to get the script hash and if it's too long, cache it at app startup?
7373
BitcoinAddress.Create(publicAddress, GetNetwork currency) |> GetElectrumScriptHashFromAddress
7474

75-
let internal GetPublicAddressFromPublicKey currency (publicKey: PubKey) =
75+
let internal GetSegwitP2shPublicAddressFromPublicKey currency (publicKey: PubKey) =
76+
publicKey
77+
.GetScriptPubKey(ScriptPubKeyType.SegwitP2SH)
78+
.GetDestinationAddress(GetNetwork currency)
79+
.ToString()
80+
81+
let internal GetNativeSegwitPublicAddressFromPublicKey currency (publicKey: PubKey) =
7682
publicKey
7783
.GetScriptPubKey(ScriptPubKeyType.Segwit)
78-
.Hash
79-
.GetAddress(GetNetwork currency)
84+
.GetDestinationAddress(GetNetwork currency)
8085
.ToString()
86+
87+
let internal GetPublicAddressFromPublicKey =
88+
if Config.UseNativeSegwit then
89+
GetNativeSegwitPublicAddressFromPublicKey
90+
else
91+
GetSegwitP2shPublicAddressFromPublicKey
8192

8293
let internal GetPublicAddressFromNormalAccountFile (currency: Currency) (accountFile: FileRepresentation): string =
8394
let pubKey = PubKey(accountFile.Name)
@@ -139,11 +150,17 @@ module Account =
139150
(mode: ServerSelectionMode)
140151
(cancelSourceOption: Option<CustomCancelSource>)
141152
: Async<BlockchainScriptHashGetBalanceInnerResult> =
142-
let scriptHashHex = GetElectrumScriptHashFromPublicAddress account.Currency account.PublicAddress
153+
let scriptHashesHex =
154+
[
155+
GetNativeSegwitPublicAddressFromPublicKey account.Currency account.PublicKey
156+
|> GetElectrumScriptHashFromPublicAddress account.Currency
157+
GetSegwitP2shPublicAddressFromPublicKey account.Currency account.PublicKey
158+
|> GetElectrumScriptHashFromPublicAddress account.Currency
159+
]
143160

144161
let querySettings =
145162
QuerySettings.Balance(mode,(BalanceMatchWithCacheOrInitialBalance account.PublicAddress account.Currency))
146-
let balanceJob = ElectrumClient.GetBalance scriptHashHex
163+
let balanceJob = ElectrumClient.GetBalances scriptHashesHex
147164
Server.Query account.Currency querySettings balanceJob cancelSourceOption
148165

149166
let private GetBalancesFromServer (account: IUtxoAccount)
@@ -176,9 +193,21 @@ module Account =
176193
let txHash = uint256 inputOutpointInfo.TransactionHash
177194
let scriptPubKeyInBytes = NBitcoin.DataEncoders.Encoders.Hex.DecodeData inputOutpointInfo.DestinationInHex
178195
let scriptPubKey = Script(scriptPubKeyInBytes)
196+
// We convert the scriptPubKey to address temporarily to compare it with
197+
// our own addresses, we could compare scriptPubKeys directly but we would
198+
// need functions that return scriptPubKey of our addresses instead of a
199+
// string.
200+
let sourceAddress = scriptPubKey.GetDestinationAddress(GetNetwork account.Currency).ToString()
179201
let coin =
180202
Coin(txHash, uint32 inputOutpointInfo.OutputIndex, Money(inputOutpointInfo.ValueInSatoshis), scriptPubKey)
181-
coin.ToScriptCoin account.PublicKey.WitHash.ScriptPubKey :> ICoin
203+
if sourceAddress = GetSegwitP2shPublicAddressFromPublicKey account.Currency account.PublicKey then
204+
coin.ToScriptCoin(account.PublicKey.WitHash.ScriptPubKey) :> ICoin
205+
elif sourceAddress = GetNativeSegwitPublicAddressFromPublicKey account.Currency account.PublicKey then
206+
coin :> ICoin
207+
else
208+
//We filter utxos based on scriptPubKey when retrieving from electrum
209+
//so this is unreachable.
210+
failwith "Unreachable: unrecognized scriptPubKey"
182211

183212
let private CreateTransactionAndCoinsToBeSigned (account: IUtxoAccount)
184213
(transactionInputs: List<TransactionInputOutpointInfo>)
@@ -294,9 +323,28 @@ module Account =
294323
else
295324
newAcc,tail
296325

297-
let job = GetElectrumScriptHashFromPublicAddress account.Currency account.PublicAddress
298-
|> ElectrumClient.GetUnspentTransactionOutputs
299-
let! utxos = Server.Query account.Currency (QuerySettings.Default ServerSelectionMode.Fast) job None
326+
let currency = account.Currency
327+
328+
let getUtxos (publicAddress: string) =
329+
async {
330+
let job = GetElectrumScriptHashFromPublicAddress currency publicAddress
331+
|> ElectrumClient.GetUnspentTransactionOutputs
332+
333+
return! Server.Query currency (QuerySettings.Default ServerSelectionMode.Fast) job None
334+
}
335+
336+
let! utxos =
337+
async {
338+
let! nativeSegwitUtxos =
339+
GetNativeSegwitPublicAddressFromPublicKey currency account.PublicKey
340+
|> getUtxos
341+
342+
let! legacySegwitUtxos =
343+
GetSegwitP2shPublicAddressFromPublicKey currency account.PublicKey
344+
|> getUtxos
345+
346+
return Seq.concat [ nativeSegwitUtxos; legacySegwitUtxos ]
347+
}
300348

301349
if not (utxos.Any()) then
302350
failwith "No UTXOs found!"

0 commit comments

Comments
 (0)