Skip to content

Commit

Permalink
Backend/Utxo: support native segwit
Browse files Browse the repository at this point in the history
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
  • Loading branch information
aarani authored and knocte committed Jan 19, 2024
1 parent 0142237 commit 99a29b4
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 15 deletions.
4 changes: 2 additions & 2 deletions src/GWallet.Backend.Tests/ElectrumIntegrationTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

[<Test>]
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

[<Test>]
member __.``can get list UTXOs of an address from some electrum BTC servers``() =
Expand Down
2 changes: 2 additions & 0 deletions src/GWallet.Backend/Config.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/GWallet.Backend/ServerManager.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 26 additions & 3 deletions src/GWallet.Backend/UtxoCoin/ElectrumClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ module ElectrumClient =
| { Encrypted = false; Protocol = Tcp port } ->
Init electrumServer.ServerInfo.NetworkPath port

let GetBalance (scriptHash: string) (stratumServer: Async<StratumClient>) = async {
let GetBalances (scriptHashes: List<string>) (stratumServer: Async<StratumClient>) = 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
Expand All @@ -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<string>) (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<StratumClient>) = async {
Expand Down
66 changes: 57 additions & 9 deletions src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -139,11 +150,17 @@ module Account =
(mode: ServerSelectionMode)
(cancelSourceOption: Option<CustomCancelSource>)
: Async<BlockchainScriptHashGetBalanceInnerResult> =
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)
Expand Down Expand Up @@ -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<TransactionInputOutpointInfo>)
Expand Down Expand Up @@ -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!"
Expand Down

0 comments on commit 99a29b4

Please sign in to comment.