diff --git a/src/GWallet.Backend.Tests/SilentPayments.fs b/src/GWallet.Backend.Tests/SilentPayments.fs index 11062545c..2ab97bdce 100644 --- a/src/GWallet.Backend.Tests/SilentPayments.fs +++ b/src/GWallet.Backend.Tests/SilentPayments.fs @@ -76,7 +76,7 @@ type SilentPayments() = let stream = BitcoinStream(DataEncoders.Encoders.Hex.DecodeData hex) Some <| WitScript.Load stream let spInput = - SilentPayments.convertToSilentPaymentInput + SilentPayments.ConvertToSilentPaymentInput (Script.FromHex input.ScriptPubKey) (DataEncoders.Encoders.Hex.DecodeData input.ScriptSig) witness @@ -104,9 +104,9 @@ type SilentPayments() = | [], Some _ -> Assert.Fail(sprintf "No inputs for shared secret derivation in test case '%s'" testCaseName) | _, Some expectedOutputString -> - let output = SilentPayments.createOutput privateKeys outpoints recipients.[0] + let output = SilentPayments.CreateOutput privateKeys outpoints recipients.[0] let outputString = output.GetEncoded() |> DataEncoders.Encoders.Hex.EncodeData Assert.AreEqual(expectedOutputString, outputString, sprintf "Failure in test case '%s'" testCaseName) | _, None -> - Assert.Throws(fun () -> SilentPayments.createOutput privateKeys outpoints recipients.[0] |> ignore) + Assert.Throws(fun () -> SilentPayments.CreateOutput privateKeys outpoints recipients.[0] |> ignore) |> ignore diff --git a/src/GWallet.Backend/UtxoCoin/SilentPayments.fs b/src/GWallet.Backend/UtxoCoin/SilentPayments.fs index 98c169a11..21ceacaf8 100644 --- a/src/GWallet.Backend/UtxoCoin/SilentPayments.fs +++ b/src/GWallet.Backend/UtxoCoin/SilentPayments.fs @@ -29,6 +29,10 @@ type SilentPaymentAddress = static member MainNetPrefix = "sp" static member TestNetPrefix = "tsp" + // https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#address-encoding + static member MinimumEncodedLength = 116u + static member MaximumEncodedLength = 1023u + static member private GetEncoder(chainName: ChainName) = let hrp = if chainName = ChainName.Mainnet then @@ -81,20 +85,20 @@ type SilentPaymentInput = | InputJustForSpending module SilentPayments = - let private secp256k1 = EC.CustomNamedCurves.GetByName("secp256k1") + let private secp256k1 = EC.CustomNamedCurves.GetByName "secp256k1" let private scalarOrder = BigInteger("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16) - let private NUMS_H_bytes = + let private NUMS_H_BYTES = [| 0x50uy; 0x92uy; 0x9buy; 0x74uy; 0xc1uy; 0xa0uy; 0x49uy; 0x54uy; 0xb7uy; 0x8buy; 0x4buy; 0x60uy; 0x35uy; 0xe9uy; 0x7auy; 0x5euy; 0x07uy; 0x8auy; 0x5auy; 0x0fuy; 0x28uy; 0xecuy; 0x96uy; 0xd5uy; 0x47uy; 0xbfuy; 0xeeuy; 0x9auy; 0xceuy; 0x80uy; 0x3auy; 0xc0uy; |] - type BigInteger with - static member FromByteArrayUnsigned (bytes: array) = + module BigInteger = + let FromByteArrayUnsigned (bytes: array) = BigInteger(1, bytes) // see https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#selecting-inputs - let convertToSilentPaymentInput (scriptPubKey: Script) (scriptSig: array) (witness: Option): SilentPaymentInput = + let ConvertToSilentPaymentInput (scriptPubKey: Script) (scriptSig: array) (witness: Option): SilentPaymentInput = if scriptPubKey.IsScriptType ScriptType.P2PKH then // skip the first 3 op_codes and grab the 20 byte hash // from the scriptPubKey @@ -144,7 +148,7 @@ module SilentPayments = let controlBlock = witnessStack.Peek() // controlBlock is <32 byte internal key> and 0 or more <32 byte hash> let internalKey = controlBlock.[1..32] - internalKey = NUMS_H_bytes + internalKey = NUMS_H_BYTES else false if internalKeyIsH then @@ -165,7 +169,7 @@ module SilentPayments = else InvalidInput - let taggedHash (tag: string) (data: array) : array = + let TaggedHash (tag: string) (data: array) : array = let sha256 = Digests.Sha256Digest() let tag = Text.ASCIIEncoding.ASCII.GetBytes tag @@ -181,12 +185,12 @@ module SilentPayments = sha256.DoFinal(result, 0) |> ignore result - let getInputHash (outpoints: List) (sumInputPubKeys: EC.ECPoint) : array = + let GetInputHash (outpoints: List) (sumInputPubKeys: EC.ECPoint) : array = let lowestOutpoint = outpoints |> List.map (fun outpoint -> outpoint.ToBytes()) |> List.min let hashInput = Array.append lowestOutpoint (sumInputPubKeys.GetEncoded true) - taggedHash "BIP0352/Inputs" hashInput + TaggedHash "BIP0352/Inputs" hashInput - let createOutput (privateKeys: List) (outpoints: List) (spAddress: SilentPaymentAddress) = + let CreateOutput (privateKeys: List) (outpoints: List) (spAddress: SilentPaymentAddress) = if privateKeys.IsEmpty then failwith "privateKeys should not be empty" @@ -211,7 +215,7 @@ module SilentPayments = if aSum = BigInteger.Zero then failwith "Input privkeys sum is zero" - let inputHash = getInputHash outpoints (secp256k1.G.Multiply aSum) + let inputHash = GetInputHash outpoints (secp256k1.G.Multiply aSum) let tweak = BigInteger.FromByteArrayUnsigned inputHash let tweakedSumSeckey = aSum.Multiply(tweak).Mod(scalarOrder) @@ -220,11 +224,21 @@ module SilentPayments = let k = 0u let tK = - taggedHash + TaggedHash "BIP0352/SharedSecret" (Array.append (ecdhSharedSecret.GetEncoded true) (BitConverter.GetBytes k)) |> BigInteger.FromByteArrayUnsigned - let Bm = secp256k1.Curve.DecodePoint <| spAddress.SpendPublicKey.ToBytes() - let sharedSecret = Bm.Add(secp256k1.G.Multiply tK) + let bM = secp256k1.Curve.DecodePoint <| spAddress.SpendPublicKey.ToBytes() + let sharedSecret = bM.Add(secp256k1.G.Multiply tK) sharedSecret.Normalize().AffineXCoord + + let GetFinalDestination (privateKey: Key) (outpoints: List) (destination: string) (network: Network) : string = + let privateKeys = + outpoints + |> List.map (fun _ -> (privateKey, false)) + + let output = CreateOutput privateKeys outpoints (SilentPaymentAddress.Decode destination) + let taprootAddress = TaprootAddress(TaprootPubKey(output.GetEncoded()), network) + + taprootAddress.ToString() diff --git a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs index 308d73e3a..a24ede0f1 100644 --- a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs +++ b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs @@ -291,7 +291,7 @@ module Account = let private CalculateSilentPaymentDestination (account: IUtxoAccount) (transactionInputs: List) - (destination: string) + (reusableAddress: string) (privateKey: Key) = // Since we can only receive P2WPKH-P2SH or P2WPKH transactions, and // non-compressed keys are not accepted as default policy @@ -302,15 +302,8 @@ module Account = let outpoints = transactionInputs |> List.map (fun input -> (ConvertToICoin account input).Outpoint) - - let privateKeys = - outpoints - |> List.map (fun _ -> (privateKey, false)) - - let output = SilentPayments.createOutput privateKeys outpoints (SilentPaymentAddress.Decode destination) - let taprootAddress = TaprootAddress(TaprootPubKey(output.GetEncoded()), GetNetwork (account:>IAccount).Currency) - - taprootAddress.ToString() + + SilentPayments.GetFinalDestination privateKey outpoints reusableAddress (GetNetwork (account:>IAccount).Currency) let private CheckSilentPaymentTransactionInputValidity (finalTransaction: Transaction) (txMetadataInputs: List) = @@ -324,11 +317,11 @@ module Account = | None -> failwith (SPrintF1 "Could not find output with index=%d in txMetadataInputs" input.PrevOut.N) let scriptPubKey = Script(NBitcoin.DataEncoders.Encoders.Hex.DecodeData inputOutpointInfo.DestinationInHex) - match SilentPayments.convertToSilentPaymentInput scriptPubKey (input.ScriptSig.ToBytes()) (Some input.WitScript) with + match SilentPayments.ConvertToSilentPaymentInput scriptPubKey (input.ScriptSig.ToBytes()) (Some input.WitScript) with | InputForSharedSecretDerivation _ -> () | _ -> let errorMessage = SPrintF1 "One of the inputs is not valid for shared secret derivation: %A" inputOutpointInfo - failwith ("Error creating silent payment transaction:\n" + errorMessage) + failwith (SPrintF1 "Error creating silent payment transaction: %s" errorMessage) let internal EstimateTransferFee (account: IUtxoAccount) @@ -427,7 +420,7 @@ module Account = raise <| Exception(SPrintF1 "Could not create fee rate from %s btc per KB" (btcPerKiloByteForFastTrans.ToString()), ex) - let destination = + let finalDestination = if SilentPaymentAddress.IsSilentPaymentAddress destination then // calculate bogus silent payment destinantion using throwaway private key, // as it will only be used for fee estimation @@ -438,7 +431,7 @@ module Account = let transactionBuilder = CreateTransactionAndCoinsToBeSigned account initiallyUsedInputs - destination + finalDestination amount try @@ -486,13 +479,13 @@ module Account = let isSilentPayment = SilentPaymentAddress.IsSilentPaymentAddress destination - let destination = + let finalDestination = if isSilentPayment then CalculateSilentPaymentDestination account txMetadata.Inputs destination privateKey else destination - let finalTransactionBuilder = CreateTransactionAndCoinsToBeSigned account txMetadata.Inputs destination amount + let finalTransactionBuilder = CreateTransactionAndCoinsToBeSigned account txMetadata.Inputs finalDestination amount finalTransactionBuilder.AddKeys privateKey |> ignore finalTransactionBuilder.SendFees (Money.Satoshis btcMinerFee.EstimatedFeeInSatoshis) @@ -725,8 +718,9 @@ module Account = // (FIXME: this is only valid for the first version of segwit, fix it!) Fixed [ 42u; 62u ] | Currency.BTC, _ when SilentPaymentAddress.IsSilentPaymentAddress address -> - // https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#address-encoding - Variable { Minimum = 116u; Maximum = 1023u } + Variable + { Minimum = SilentPaymentAddress.MinimumEncodedLength + Maximum = SilentPaymentAddress.MaximumEncodedLength } | Currency.LTC, _ when address.StartsWith LITECOIN_ADDRESS_BECH32_PREFIX -> // taken from https://coin.space/all-about-address-types/, e.g. ltc1q3qkpj5s4ru3cx9t7dt27pdfmz5aqy3wplamkns // FIXME: hopefully someone replies/documents https://bitcoin.stackexchange.com/questions/110975/how-long-can-bech32-addresses-be-in-the-litecoin-mainnet