diff --git a/.gitignore b/.gitignore index b0ada04..abaf17d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /src/NLitecoin/bin /tests/NLitecoin.Tests/obj /tests/NLitecoin.Tests/bin +/tests/NLitecoin.MimbleWimble.Tests/obj +/tests/NLitecoin.MimbleWimble.Tests/bin \ No newline at end of file diff --git a/NLitecoin.sln b/NLitecoin.sln index 1422006..ef2aa14 100644 --- a/NLitecoin.sln +++ b/NLitecoin.sln @@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.6.33829.357 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NLitecoin", "src\NLitecoin\NLitecoin.fsproj", "{1128287F-0435-4FA9-B4DD-420A9E1FDB23}" +Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "NLitecoin", "src\NLitecoin\NLitecoin.fsproj", "{1128287F-0435-4FA9-B4DD-420A9E1FDB23}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLitecoin.Tests", "tests\NLitecoin.Tests\NLitecoin.Tests.csproj", "{F954D3AC-36BC-483E-8DB1-4CC65DF1694D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NLitecoin.Tests", "tests\NLitecoin.Tests\NLitecoin.Tests.csproj", "{F954D3AC-36BC-483E-8DB1-4CC65DF1694D}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NLitecoin.MimbleWimble.Tests", "tests\NLitecoin.MimbleWimble.Tests\NLitecoin.MimbleWimble.Tests.fsproj", "{6A67D54D-BE2C-449F-88D2-D6739A5F3F89}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -21,6 +23,10 @@ Global {F954D3AC-36BC-483E-8DB1-4CC65DF1694D}.Debug|Any CPU.Build.0 = Debug|Any CPU {F954D3AC-36BC-483E-8DB1-4CC65DF1694D}.Release|Any CPU.ActiveCfg = Release|Any CPU {F954D3AC-36BC-483E-8DB1-4CC65DF1694D}.Release|Any CPU.Build.0 = Release|Any CPU + {6A67D54D-BE2C-449F-88D2-D6739A5F3F89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A67D54D-BE2C-449F-88D2-D6739A5F3F89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A67D54D-BE2C-449F-88D2-D6739A5F3F89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A67D54D-BE2C-449F-88D2-D6739A5F3F89}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/NLitecoin/Litecoin.fs b/src/NLitecoin/Litecoin.fs index 318c7b8..5c347e8 100644 --- a/src/NLitecoin/Litecoin.fs +++ b/src/NLitecoin/Litecoin.fs @@ -21,7 +21,7 @@ type internal Witness(inputs: TxInList) = | null -> WitScript.Empty | witScript -> witScript script.ToBytes() - stream.ReadWrite(ref bytes) + stream.ReadWrite bytes else input.WitScript <- WitScript.Load stream @@ -50,10 +50,14 @@ module private TxListExtensions = type LitecoinTransaction() = inherit Transaction() + static let mwebExtensionTxFlag = 8uy + + member val MimbleWimbleTransaction: Option = None with get, set + override self.GetConsensusFactory() = LitecoinConsensusFactory.Instance member private self.Read (stream: BitcoinStream) (witSupported: bool) = - let flags = 0uy + let mutable flags = 0uy self.nVersion <- stream.ReadWrite self.nVersion // Try to read the vin. In case the dummy is there, this will be read as an empty vector. stream.ReadWrite(&self.vin) @@ -64,7 +68,7 @@ type LitecoinTransaction() = if self.vin.Count = 0 && witSupported && not hasNoDummy then // We read a dummy or an empty vin. - let flags = stream.ReadWrite flags + flags <- stream.ReadWrite flags if flags <> 0uy then // Assume we read a dummy and a flag. stream.ReadWrite(&self.vin) @@ -79,22 +83,20 @@ type LitecoinTransaction() = stream.ReadWrite(&self.vout) self.vout <- self.vout.WithTransaction self - let flags = - if ((flags &&& 1uy) <> 0uy) && witSupported then - // The witness flag is present, and we support witnesses. - let wit = Witness self.Inputs - wit.ReadWrite stream - flags ^^^ 1uy - else - flags - let flags = - if (flags &&& 8uy) <> 0uy then //MWEB extension tx flag - (* The MWEB flag is present, but currently no MWEB data is supported. - * This fix just prevent from throwing exception bellow so cannonical litecoin transaction can be read - *) - flags ^^^ 8uy + if ((flags &&& 1uy) <> 0uy) && witSupported then + // The witness flag is present, and we support witnesses. + let wit = Witness self.Inputs + wit.ReadWrite stream + flags <- flags ^^^ 1uy + + if (flags &&& mwebExtensionTxFlag) <> 0uy then + let isMWTransactionPresent = stream.ReadWrite Unchecked.defaultof + if isMWTransactionPresent <> 0uy then + self.MimbleWimbleTransaction <- Some(MimbleWimble.Transaction.Read stream) else - flags + // HogEx transaction + self.MimbleWimbleTransaction <- None + flags <- flags ^^^ 8uy if flags <> 0uy then // Unknown flag in the serialization @@ -118,6 +120,12 @@ type LitecoinTransaction() = else 0uy + let flags = + if self.MimbleWimbleTransaction.IsSome then + flags ||| mwebExtensionTxFlag + else + flags + let flags = if flags <> 0uy then // Use extended format in case witnesses are to be serialized. @@ -135,6 +143,13 @@ type LitecoinTransaction() = let wit = Witness self.Inputs wit.ReadWrite stream + match self.MimbleWimbleTransaction with + | Some mwebTransaction -> + let valueIsPresentMarker = 1uy + stream.ReadWrite valueIsPresentMarker |> ignore + (mwebTransaction :> MimbleWimble.ISerializeable).Write stream + | None -> () + override self.ReadWrite(stream: BitcoinStream) = let witSupported = (((uint32 stream.TransactionOptions) &&& (uint32 TransactionOptions.Witness)) <> 0u) && diff --git a/src/NLitecoin/MimbleWimble/Bulletproof.fs b/src/NLitecoin/MimbleWimble/Bulletproof.fs new file mode 100644 index 0000000..f0d9484 --- /dev/null +++ b/src/NLitecoin/MimbleWimble/Bulletproof.fs @@ -0,0 +1,732 @@ +module NLitecoin.MimbleWimble.Bulletproof + +open System + +open Org.BouncyCastle.Crypto +open Org.BouncyCastle.Crypto.Digests +open Org.BouncyCastle.Crypto.Parameters +open Org.BouncyCastle.Math +open Org.BouncyCastle.Math.EC +open NBitcoin + +open EC + +type HmacSha256(key: array) = + let outer = Sha256Digest() + let inner = Sha256Digest() + do + let rKey = Array.zeroCreate 64 + Array.blit key 0 rKey 0 key.Length + + for n=0 to rKey.Length-1 do + rKey.[n] <- rKey.[n] ^^^ 0x5cuy + outer.BlockUpdate(rKey, 0, 64) + + for n=0 to rKey.Length-1 do + rKey.[n] <- rKey.[n] ^^^ 0x5cuy ^^^ 0x36uy + inner.BlockUpdate(rKey, 0, 64) + + member self.Write(data: array) = + inner.BlockUpdate(data, 0, data.Length) + + member self.Finalize(out32: array) = + assert(out32.Length = 32) + let temp = Array.zeroCreate 32 + inner.DoFinal(temp, 0) |> ignore + outer.BlockUpdate(temp, 0, 32) + outer.DoFinal(out32, 0) |> ignore + +type Rfc6979HmacSha256(key: array) = + let k = Array.create 32 0uy + let v = Array.create 32 1uy + + do + let hmac = HmacSha256 k + hmac.Write v + hmac.Write [| 0uy |] + hmac.Write key + hmac.Finalize k + let hmac = HmacSha256 k + hmac.Write v + hmac.Finalize v + + let hmac = HmacSha256 k + hmac.Write v + hmac.Write [| 1uy |] + hmac.Write key + hmac.Finalize k + let hmac = HmacSha256 k + hmac.Write v + hmac.Finalize v + + let mutable retry = false + + member self.Generate(outLen: int) : array = + if retry then + let hmac = HmacSha256 k + hmac.Write v + hmac.Write [| 0uy |] + hmac.Finalize k + let hmac = HmacSha256 k + hmac.Write v + hmac.Finalize v + + let mutable outLen = outLen + let out = ResizeArray() + while outLen > 0 do + let now = min 32 outLen + let hmac = HmacSha256 k + hmac.Write v + hmac.Finalize v + out.AddRange(v |> Array.take now) + outLen <- outLen - now + + retry <- true + + out.ToArray() + +let ShallueVanDeWoestijne(t: ECFieldElement) : ECPoint = + let c = + BigInteger("0a2d2ba93507f1df233770c2a797962cc61f6d15da14ecd47d8d27ae1cd5f852", 16) + |> curve.Curve.FromBigInteger + let d = + BigInteger("851695d49a83f8ef919bb86153cbcb16630fb68aed0a766a3ec693d68e6afa40", 16) + |> curve.Curve.FromBigInteger + let b = curve.Curve.FromBigInteger(BigInteger.ValueOf 7L) + + let w = c.Multiply(t).Divide(b.AddOne().Add(t.Square())) + let x1 = d.Subtract(t.Multiply w) + let x2 = x1.AddOne().Negate() + let x3 = w.Square().Invert().AddOne() + + let alphaIn = x1.Square().Multiply(x1).Add(b) + let betaIn = x2.Square().Multiply(x2).Add(b) + let gammaIn = x3.Square().Multiply(x3).Add(b) + + let alphaQuad = IsQuadVar alphaIn + let y1 = alphaIn.Sqrt() + let betaquad = IsQuadVar betaIn + let y2 = betaIn.Sqrt() + let y3 = gammaIn.Sqrt() + + let x1 = if (not alphaQuad) && betaquad then x2 else x1 + let y1 = if (not alphaQuad) && betaquad then y2 else y1 + let x1 = if (not alphaQuad) && not betaquad then x3 else x1 + let y1 = if (not alphaQuad) && not betaquad then y3 else y1 + + let res = curve.Curve.CreatePoint(x1.ToBigInteger(), y1.ToBigInteger()) + if t.ToBigInteger().Mod(BigInteger.Two) = BigInteger.One then + curve.Curve.CreatePoint( + res.XCoord.ToBigInteger(), + res.YCoord.Negate().ToBigInteger() + ) + else + res + +let GeneratorGenerate (key: array) : ECPoint = + let prefix1 = "1st generation: " |> Text.ASCIIEncoding.ASCII.GetBytes + let prefix2 = "2nd generation: " |> Text.ASCIIEncoding.ASCII.GetBytes + let sha256 = Sha256Digest() + sha256.BlockUpdate(prefix1, 0, 16) + sha256.BlockUpdate(key, 0, 32) + let b32 = Array.zeroCreate 32 + sha256.DoFinal(b32, 0) |> ignore + let t = BigInteger.FromByteArrayUnsigned b32 |> curve.Curve.FromBigInteger + let accum = ShallueVanDeWoestijne t + + let sha256 = Sha256Digest() + sha256.BlockUpdate(prefix2, 0, 16) + sha256.BlockUpdate(key, 0, 32) + sha256.DoFinal(b32, 0) |> ignore + let t = BigInteger.FromByteArrayUnsigned b32 |> curve.Curve.FromBigInteger + let accum = accum.Add(ShallueVanDeWoestijne t) + + accum.Normalize() + +let GetGenerators (n: int) : array = + let seed = Array.append (generatorG.XCoord.GetEncoded()) (generatorG.YCoord.GetEncoded()) + let rng = Rfc6979HmacSha256 seed + Array.init + n + (fun _ -> GeneratorGenerate (rng.Generate 32)) + +let ScalarDotProduct (vec1: array, vec2: array) : BigInteger = + (Array.map2 (fun (x : BigInteger) y -> x.Multiply y) vec1 vec2 + |> Array.fold (fun (x : BigInteger) y -> x.Add y) BigInteger.Zero) + .Mod(scalarOrder) + +// port of https://github.com/litecoin-project/litecoin/blob/5ac781487cc9589131437b23c69829f04002b97e/src/secp256k1-zkp/src/scalar.h#L114 +let ScalarChaCha20 (seed: uint256) (index: uint64) : BigInteger * BigInteger = + let seed32 = seed.ToBytes() |> Array.chunkBySize 4 |> Array.map BitConverter.ToUInt32 + + let inline LE32 p = + if BitConverter.IsLittleEndian then + p + else + ((p &&& 0xFFu) <<< 24) ||| ((p &&& 0xFF00u) <<< 8) ||| (((p) &&& 0xFF0000u) >>> 8) ||| (((p) &&& 0xFF000000u) >>> 24) + + let inline BE32 p = + if BitConverter.IsLittleEndian then + ((p &&& 0xFFUL) <<< 24) ||| ((p &&& 0xFF00UL) <<< 8) ||| ((p &&& 0xFF0000UL) >>> 8) ||| ((p &&& 0xFF000000UL) >>> 24) + else + p + + let inline ROTL32(x: uint32, n) = ((x) <<< (n)) ||| ((x) >>> (32-(n))) + + let QUARTERROUND (x: array) (a,b,c,d) = + x.[a] <- x.[a] + x.[b] + x.[d] <- ROTL32(x.[d] ^^^ x.[a], 16) + x.[c] <- x.[c] + x.[d] + x.[b] <- ROTL32(x.[b] ^^^ x.[c], 12) + x.[a] <- x.[a] + x.[b] + x.[d] <- ROTL32(x.[d] ^^^ x.[a], 8) + x.[c] <- x.[c] + x.[d] + x.[b] <- ROTL32(x.[b] ^^^ x.[c], 7) + + let createScalar (arr: array) = + let result = + arr + |> Array.map BitConverter.GetBytes + |> Array.concat + |> Array.rev + |> BigInteger.FromByteArrayUnsigned + if result >= scalarOrder then + None + else + Some result + + let rec produceScalars (overCount: uint32) : BigInteger * BigInteger = + let x = [| + 0x61707865u + 0x3320646eu + 0x79622d32u + 0x6b206574u + LE32(seed32.[0]) + LE32(seed32.[1]) + LE32(seed32.[2]) + LE32(seed32.[3]) + LE32(seed32.[4]) + LE32(seed32.[5]) + LE32(seed32.[6]) + LE32(seed32.[7]) + uint32 index + uint32(index >>> 32) + 0u + overCount + |] + + for _=1 to 10 do + QUARTERROUND x (0, 4, 8,12) + QUARTERROUND x (1, 5, 9,13) + QUARTERROUND x (2, 6,10,14) + QUARTERROUND x (3, 7,11,15) + QUARTERROUND x (0, 5,10,15) + QUARTERROUND x (1, 6,11,12) + QUARTERROUND x (2, 7, 8,13) + QUARTERROUND x (3, 4, 9,14) + + let x = + Array.map2 + (+) + x + [| + 0x61707865u + 0x3320646eu + 0x79622d32u + 0x6b206574u + LE32(seed32.[0]) + LE32(seed32.[1]) + LE32(seed32.[2]) + LE32(seed32.[3]) + LE32(seed32.[4]) + LE32(seed32.[5]) + LE32(seed32.[6]) + LE32(seed32.[7]) + uint32 index + uint32(index >>> 32) + 0u + overCount + |] + let r1 = + [| + BE32(uint64 x.[6]) <<< 32 ||| BE32(uint64 x.[7]) + BE32(uint64 x.[4]) <<< 32 ||| BE32(uint64 x.[5]) + BE32(uint64 x.[2]) <<< 32 ||| BE32(uint64 x.[3]) + BE32(uint64 x.[0]) <<< 32 ||| BE32(uint64 x.[1]) + |] + |> createScalar + let r2 = + [| + BE32(uint64 x.[14]) <<< 32 ||| BE32(uint64 x.[15]) + BE32(uint64 x.[12]) <<< 32 ||| BE32(uint64 x.[13]) + BE32(uint64 x.[10]) <<< 32 ||| BE32(uint64 x.[11]) + BE32(uint64 x.[8]) <<< 32 ||| BE32(uint64 x.[9]) + |] + |> createScalar + + match r1, r2 with + | Some sc1, Some sc2 -> sc1, sc2 + | _ -> produceScalars (overCount + 1u) + + produceScalars 0u + +let UpdateCommit (commit: uint256) (lpt: ECPoint) (rpt: ECPoint) : uint256 = + let lpt = lpt.Normalize() + let rpt = rpt.Normalize() + + let lrparity = + (if IsQuadVar lpt.AffineYCoord then 0uy else 2uy) + + (if IsQuadVar rpt.AffineYCoord then 0uy else 1uy) + + let hasher = Sha256Digest() + hasher.BlockUpdate(commit.ToBytes(), 0, 32) + hasher.Update lrparity + hasher.BlockUpdate(lpt.AffineXCoord.GetEncoded(), 0, 32) + hasher.BlockUpdate(rpt.AffineXCoord.GetEncoded(), 0, 32) + + let result = Array.zeroCreate 32 + hasher.DoFinal(result, 0) |> ignore + result |> uint256 + +let SerializePoints (points: array) (proof: Span) = + let bitVecLen = (points.Length + 7) / 8 + proof.Slice(0, bitVecLen).Fill 0uy + + for i, point in points |> Seq.indexed do + let pointNormalized = point.Normalize() + pointNormalized.XCoord.GetEncoded().CopyTo(proof.Slice(bitVecLen + i * 32)) + if not(IsQuadVar pointNormalized.YCoord) then + proof.[i / 8] <- proof.[i / 8] ||| uint8(1 <<< (i % 8)) + +let private LrGenerate + (nonce: uint256) + (y: BigInteger) + (z: BigInteger) + (nbits: int) + (value: uint64) + (x: BigInteger) + : seq = + let initailState = + {| + Result = (BigInteger.Zero, BigInteger.Zero) + Z22n = z.Square() + Yn = BigInteger.One + |} + + Seq.init nbits id + |> Seq.scan + (fun (state : {| Result: (BigInteger * BigInteger); Yn: BigInteger; Z22n: BigInteger |}) bitIdx -> + let bit = int64((value >>> bitIdx) &&& 1UL) + let count = bitIdx + + let sl, sr = ScalarChaCha20 nonce (uint64(count + 2)) + let al = BigInteger.ValueOf bit + let ar = BigInteger.ValueOf(1L - bit).Negate() + + let lOut = al.Subtract(z).Add(sl.Multiply x) + let rOut = ar.Add(z).Add(sr.Multiply x).Multiply(state.Yn).Add(state.Z22n) + + {| + Result = (lOut.Mod scalarOrder, rOut.Mod scalarOrder) + Yn = state.Yn.Multiply(y).Mod(scalarOrder) + Z22n = state.Z22n.Add(state.Z22n).Mod(scalarOrder) + |}) + initailState + |> Seq.map (fun state -> state.Result) + // skip first value since it's a copy of initailState + |> Seq.skip 1 + +let IP_AB_SCALARS = 4 + +let PopCount n = + let mutable ret = 0 + let mutable x = n + for i=0 to 63 do + ret <- ret + x &&& 1 + x <- x >>> 1 + ret + +// https://github.com/litecoin-project/litecoin/blob/5ac781487cc9589131437b23c69829f04002b97e/src/secp256k1-zkp/src/modules/bulletproofs/util.h#L11 +let FloorLog (n: uint32) = + if n = 0u then + 0u + else + System.Math.Log(float n, 2.0) |> floor |> uint32 + +let rec InnerProductRealProve + (g: ECPoint) + (geng: array) + (genh: array) + (aArr: array) + (bArr: array) + (yInv: BigInteger) + (ux: BigInteger) + (n: int) + (commit: uint256) + : array = + let SECP256K1_BULLETPROOF_MAX_DEPTH = 31 + let x = Array.create SECP256K1_BULLETPROOF_MAX_DEPTH BigInteger.Zero + let xInv = Array.create SECP256K1_BULLETPROOF_MAX_DEPTH BigInteger.Zero + + let outPts = ResizeArray() + let mutable commit = commit + + let mutable keepIterating = true + + // Protocol 1: Iterate, halving vector size until it is 1 + let vSizes = + Seq.unfold + (fun halfwidth -> + if halfwidth > IP_AB_SCALARS / 2 then + Some(halfwidth / 2, halfwidth / 2) + else + None) + n + vSizes + |> Seq.takeWhile (fun _ -> keepIterating) + |> Seq.iteri (fun i halfwidth -> + let grouping = (1 <<< i) + + let getLrPointsAndScalars (odd: int) (gSc: BigInteger) = + seq { + let mutable yInvN = BigInteger.One + for idx in Seq.initInfinite id do + let abIdx = (idx / grouping) ^^^ 1 + // Special-case the primary generator + if idx = n then + yield g, gSc + else + // steps 1/2 + let pt, sc = + if idx / grouping % 2 = odd then + let sc = bArr.[abIdx].Multiply yInvN + genh.[idx], sc + else + geng.[idx], aArr.[abIdx] + // step 3 + let mutable sc = sc + let groupings = + Seq.initInfinite (fun i -> 1u <<< i) + |> Seq.takeWhile (fun each -> each < uint32 grouping) + groupings |> Seq.iteri (fun i gr -> + if (((idx / int gr) % 2) ^^^ ((idx / grouping) % 2)) = odd then + sc <- sc.Multiply x.[i] + else + sc <- sc.Multiply xInv.[i] + ) + + yInvN <- yInvN.Multiply(yInv).Mod(scalarOrder) + + yield pt, sc.Mod(scalarOrder) + } + + let multMultivar (pointsAndScalars: seq) (nPoints: int) = + pointsAndScalars + |> Seq.take nPoints + |> Seq.fold + (fun (acc: ECPoint) (point, scalar) -> acc.Add(point.Multiply scalar) ) + (generatorG.Multiply BigInteger.Zero) + + // L + let gSc = + ([| for j=0 to halfwidth-1 do yield aArr.[2*j], bArr.[2*j+1] |] + |> Array.unzip + |> ScalarDotProduct) + .Multiply(ux).Mod(scalarOrder) + + outPts.Add(multMultivar (getLrPointsAndScalars 0 gSc) (n + 1)) + + // R + let gSc = + ([| for j=0 to halfwidth-1 do yield aArr.[2*j+1], bArr.[2*j] |] + |> Array.unzip + |> ScalarDotProduct) + .Multiply(ux).Mod(scalarOrder) + + outPts.Add(multMultivar (getLrPointsAndScalars 1 gSc) (n + 1)) + + // x, x^2, x^-1, x^-2 + commit <- + UpdateCommit + commit + outPts.[outPts.Count - 2] + outPts.[outPts.Count - 1] + + x.[i] <- (commit.ToBytes() |> BigInteger.FromByteArrayUnsigned).Mod(scalarOrder) + xInv.[i] <- x.[i].ModInverse(scalarOrder) + + // update scalar array + for j=0 to halfwidth-1 do + aArr.[2*j] <- aArr.[2*j].Multiply(x.[i]).Mod(scalarOrder) + aArr.[j] <- aArr.[2*j].Add(aArr.[2*j+1].Multiply xInv.[i]).Mod(scalarOrder) + + bArr.[2*j] <- bArr.[2*j].Multiply(xInv.[i]).Mod(scalarOrder) + bArr.[j] <- bArr.[2*j].Add(bArr.[2*j+1].Multiply x.[i]).Mod(scalarOrder) + + // Combine G generators and recurse, if that would be more optimal + if n > 32 && i = 1 then + let getGPointsAndScalars (geng: array) = + seq { + for idx in Seq.initInfinite id do + let pt = geng.[idx] + let mutable sc = BigInteger.One + let indices = + Seq.initInfinite id + |> Seq.takeWhile (fun i -> (1 <<< i) <= grouping) + for i in indices do + if idx &&& (1 <<< i) <> 0 then + sc <- sc.Multiply x.[i] + else + sc <- sc.Multiply xInv.[i] + yield pt, sc.Mod scalarOrder + } + + let getHPointsAndScalars (genh: array) = + seq { + let mutable yInvN = BigInteger.One + for idx in Seq.initInfinite id do + let pt = genh.[idx] + let mutable sc = BigInteger.One + let indices = + Seq.initInfinite id + |> Seq.takeWhile (fun i -> (1 <<< i) <= grouping) + for i in indices do + if idx &&& (1 <<< i) <> 0 then + sc <- sc.Multiply xInv.[i] + else + sc <- sc.Multiply x.[i] + sc <- sc.Multiply yInvN + yInvN <- yInvN.Multiply(yInv).Mod(scalarOrder) + yield pt, sc.Mod scalarOrder + } + + for j=0 to halfwidth-1 do + let rG = multMultivar (getGPointsAndScalars (geng |> Array.skip (j * (2 <<< i)))) (2 <<< i) + geng.[j] <- rG + let rH = multMultivar (getHPointsAndScalars (genh |> Array.skip (j * (2 <<< i)))) (2 <<< i) + genh.[j] <- rH + + let yInv2 = + Seq.init (i + 1) ignore + |> Seq.fold + (fun (acc: BigInteger) _ -> acc.Square().Mod(scalarOrder)) + yInv + + InnerProductRealProve g geng genh aArr bArr yInv2 ux halfwidth commit + |> outPts.AddRange + // break + keepIterating <- false + ) + + outPts.ToArray() + +let InnerProductProofLength (n: int) = + if n < IP_AB_SCALARS / 2 then + 32 * (1 + 2 * n) + else + let bitCount = PopCount n + let log = FloorLog <| uint32(2 * n / IP_AB_SCALARS) + 32 * (1 + 2 * (bitCount - 1 + int log) + IP_AB_SCALARS) + int(2u * log + 7u) / 8 + +let InnerProductProve + (generators: array) + (yInv: BigInteger) + (n: int) + (lrSequence: seq) + (commitInp: array) = + let proof = Array.zeroCreate(InnerProductProofLength n) + + let aArr, bArr = lrSequence |> Seq.take n |> Seq.toArray |> Array.unzip + let geng = generators |> Array.take n + let genh = generators |> Array.skip (generators.Length / 2) |> Array.take n + + // Record final dot product + let dot = ScalarDotProduct(aArr, bArr) + dot.ToUInt256().ToBytes().CopyTo (proof.AsSpan()) + + // Protocol 2: hash dot product to obtain G-randomizer + let commit = + let hasher = Sha256Digest() + hasher.BlockUpdate(commitInp, 0, commitInp.Length) + hasher.BlockUpdate(proof, 0, 32) + let bytes = Array.zeroCreate 32 + hasher.DoFinal(bytes, 0) |> ignore + bytes + + let proofSlice = proof.AsSpan().Slice 32 + + let ux = (BigInteger.FromByteArrayUnsigned commit).Mod(scalarOrder) + + let outPts = InnerProductRealProve generatorG geng genh aArr bArr yInv ux n (uint256 commit) + + // Final a/b values + let halfNAB = min (IP_AB_SCALARS / 2) n + for i=0 to halfNAB-1 do + aArr.[i].ToUInt256().ToBytes().CopyTo(proofSlice.Slice(32 * i)) + bArr.[i].ToUInt256().ToBytes().CopyTo(proofSlice.Slice(32 * (i + halfNAB))) + + let proofSlice = proofSlice.Slice(64 * halfNAB) + + SerializePoints outPts proofSlice + + proof + +let ConstructRangeProof + (amount: uint64) + (key: uint256) + (privateNonce: uint256) + (rewindNonce: uint256) + (proofMessage: array) + (extraData: Option>) : RangeProof = + let commitp = + generatorH.Multiply(BigInteger.ValueOf(int64 amount)) + .Add(generatorG.Multiply(key.ToBytes() |> BigInteger.FromByteArrayUnsigned)) + + let commit = UpdateCommit uint256.Zero commitp generatorH + + let commit = + match extraData with + | Some bytes -> + let hasher = Sha256Digest() + hasher.BlockUpdate(commit.ToBytes(), 0, 32) + hasher.BlockUpdate(bytes, 0, bytes.Length) + let result = Array.zeroCreate 32 + hasher.DoFinal(result, 0) |> ignore + uint256 result + | None -> + commit + + let alpha, rho = ScalarChaCha20 rewindNonce 0UL + let tau1, tau2 = ScalarChaCha20 privateNonce 1UL + + // Encrypt value into alpha, so it will be recoverable from -mu by someone who knows rewindNonce + let alpha = + let vals = BigInteger.ValueOf(int64 amount) + // Combine value with 20 bytes of optional message + let vals_bytes = vals.ToUInt256().ToBytes() + for i=0 to 20-1 do + vals_bytes.[i+4] <- proofMessage.[i] + let vals = BigInteger.FromByteArrayUnsigned vals_bytes + // Negate so it'll be positive in -mu + let vals = vals.Negate() + alpha.Add(vals).Mod(scalarOrder) + + let nbits = 64 + + let generators = GetGenerators 256 + + // Compute A and S + let aL = Array.init nbits (fun i -> (amount &&& (1UL <<< i)) <> 0UL ) + + let a = + let aterms = + Seq.init nbits (fun j -> + if aL.[j] then + generators.[j] + else + generators.[j + generators.Length / 2].Negate()) + Seq.fold (fun (acc: ECPoint) p -> acc.Add p) (generatorG.Multiply alpha) aterms + + let s = + let sterms = + Seq.init nbits (fun j -> + let sl, sr = ScalarChaCha20 rewindNonce (uint64(j + 2)) + generators.[j].Multiply(sl).Add(generators.[j + generators.Length / 2].Multiply sr)) + Seq.fold (fun (acc: ECPoint) p -> acc.Add p) (generatorG.Multiply rho) sterms + + // get challenges y and z + let outPt0 = a + let outPt1 = s + let commit = UpdateCommit commit outPt0 outPt1 + let y = BigInteger.FromByteArrayUnsigned(commit.ToBytes()).Mod(scalarOrder) + let commit = UpdateCommit commit outPt0 outPt1 + let z = BigInteger.FromByteArrayUnsigned(commit.ToBytes()).Mod(scalarOrder) + + // Compute coefficients t0, t1, t2 of the polynomial + // t0 = l(0) dot r(0) + let t0 = + LrGenerate rewindNonce y z nbits amount BigInteger.Zero + |> Seq.toArray + |> Array.unzip + |> ScalarDotProduct + + // see Bulletproofs: Efficient Range Proofs for Confidential Transactions paper, p. 17 + let inline t0assertion() = + let t0alt = + let oneNyN = ScalarDotProduct(Array.create nbits BigInteger.One, Array.init nbits (fun n -> y.Pow n)) + let oneN2N = ScalarDotProduct(Array.create nbits BigInteger.One, Array.init nbits (fun n -> BigInteger.Two.Pow n)) + z.Multiply(oneNyN) + .Add(z.Square().Multiply(BigInteger.ValueOf(int64 amount))) + .Add(z.Square().Negate().Multiply(oneNyN).Subtract(z.Square().Multiply(z).Multiply(oneN2N))) + .Mod(scalarOrder) + t0alt = t0 + assert(t0assertion()) + + // A = t0 + t1 + t2 = l(1) dot r(1) + let A = + LrGenerate rewindNonce y z nbits amount BigInteger.One + |> Seq.toArray + |> Array.unzip + |> ScalarDotProduct + + // B = t0 - t1 + t2 = l(-1) dot r(-1) + let B = + LrGenerate rewindNonce y z nbits amount (BigInteger.One.Negate().Mod(scalarOrder)) + |> Seq.toArray + |> Array.unzip + |> ScalarDotProduct + + // t1 = (A - B)/2 + let t1 = A.Subtract(B).Multiply(BigInteger.Two.ModInverse scalarOrder).Mod(scalarOrder) + + // t2 = -(-B + t0) + t1 + let t2 = B.Negate().Add(t0).Negate().Add(t1).Mod(scalarOrder) + + // Compute Ti = t_i*A + tau_i*G for i = 1,2 + // Normal bulletproof: T1=t1*A + tau1*G + let outPt2 = generatorG.Multiply(tau1).Add(generatorH.Multiply t1) + let outPt3 = generatorG.Multiply(tau2).Add(generatorH.Multiply t2) + + let commit = UpdateCommit commit outPt2 outPt3 + let x = BigInteger.FromByteArrayUnsigned(commit.ToBytes()).Mod(scalarOrder) + + // compute tau_x and mu + // Negate taux and mu so the verifier doesn't have to + let tauX = + tau1 + .Multiply(x) + .Add(tau2.Multiply(x.Square())) + .Add(z.Square().Multiply(key.ToBytes() |> BigInteger.FromByteArrayUnsigned)) + .Negate() + .Mod(scalarOrder) + + let mu = rho.Multiply(x).Add(alpha).Negate().Mod(scalarOrder) + + // Encode rangeproof stuff + let proof : array = Array.zeroCreate RangeProof.Size + Array.blit (tauX.ToUInt256().ToBytes()) 0 proof 0 32 + Array.blit (mu.ToUInt256().ToBytes()) 0 proof 32 32 + SerializePoints [| outPt0; outPt1; outPt2; outPt3 |] (proof.AsSpan().Slice 64) + + // Mix this into the hash so the input to the inner product proof is fixed + let commit = + let hasher = Sha256Digest() + hasher.BlockUpdate(commit.ToBytes(), 0, 32) + hasher.BlockUpdate(proof, 0, 64) + let hash = Array.zeroCreate 32 + hasher.DoFinal(hash, 0) |> ignore + hash + + // Compute l and r, do inner product proof + let innerProductProof = + let lrSequence = LrGenerate rewindNonce y z nbits amount x + let y = y.ModInverse scalarOrder + InnerProductProve generators y nbits lrSequence commit + + let innerProductProofOffset = 64 + 128 + 1 + let innerProductProofLength = InnerProductProofLength nbits + assert(innerProductProofLength + innerProductProofOffset = RangeProof.Size) + + Array.blit innerProductProof 0 proof innerProductProofOffset innerProductProofLength + + RangeProof proof diff --git a/src/NLitecoin/MimbleWimble/EC.fs b/src/NLitecoin/MimbleWimble/EC.fs new file mode 100644 index 0000000..75eebf3 --- /dev/null +++ b/src/NLitecoin/MimbleWimble/EC.fs @@ -0,0 +1,124 @@ +module NLitecoin.MimbleWimble.EC + +open System + +open Org.BouncyCastle.Crypto.Parameters +open Org.BouncyCastle.Asn1.X9 +open Org.BouncyCastle.Math +open Org.BouncyCastle.Math.EC +open Org.BouncyCastle.Crypto.Digests + +let curve = ECNamedCurveTable.GetByName "secp256k1" +let domainParams = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed()) + +// see https://github.com/bitcoin-core/secp256k1/issues/1180#issuecomment-1356859346 +let scalarOrder = BigInteger("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16) + +let generatorG = curve.G +let generatorH = + curve.Curve.CreatePoint + (BigInteger + [| 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; |], + BigInteger + [| 0x31uy; 0xd3uy; 0xc6uy; 0x86uy; 0x39uy; 0x73uy; 0x92uy; 0x6euy; 0x04uy; 0x9euy; 0x63uy; 0x7cuy; 0xb1uy; 0xb5uy; 0xf4uy; 0x0auy; + 0x36uy; 0xdauy; 0xc2uy; 0x8auy; 0xf1uy; 0x76uy; 0x69uy; 0x68uy; 0xc3uy; 0x0cuy; 0x23uy; 0x13uy; 0xf3uy; 0xa3uy; 0x89uy; 0x04uy; |]) + +let generatorJPub = + curve.Curve.DecodePoint + [| 0x02uy; + 0xb8uy; 0x60uy; 0xf5uy; 0x67uy; 0x95uy; 0xfcuy; 0x03uy; 0xf3uy; + 0xc2uy; 0x16uy; 0x85uy; 0x38uy; 0x3duy; 0x1buy; 0x5auy; 0x2fuy; + 0x29uy; 0x54uy; 0xf4uy; 0x9buy; 0x7euy; 0x39uy; 0x8buy; 0x8duy; + 0x2auy; 0x01uy; 0x93uy; 0x93uy; 0x36uy; 0x21uy; 0x15uy; 0x5fuy; |] + +type BigInteger with + static member FromByteArrayUnsigned (bytes: array) = + BigInteger(1, bytes) + + member self.ToUInt256() = + let bytes = self.ToByteArrayUnsigned() + NBitcoin.uint256 (Array.append (Array.zeroCreate (32 - bytes.Length)) bytes) + +type NBitcoin.Secp256k1.ECPrivKey with + member self.ToBytes() = + let numBytesInPrivateKey = 32 + let bytes = Array.zeroCreate numBytesInPrivateKey + self.WriteToSpan(bytes.AsSpan()) + bytes + +let private Jakobi (elem: ECFieldElement) = + let k = curve.Curve.Field.Characteristic + let n = elem.ToBigInteger() + + // jacobi symbol calculation algorithm + let rec loop (n: BigInteger) (k: BigInteger) t = + if n = BigInteger.Zero then + n, k, t + else + let rec innerLoop (n: BigInteger) t = + if n.Mod BigInteger.Two <> BigInteger.Zero then + n, t + else + let n = n.Divide BigInteger.Two + let r = k.Mod(BigInteger.ValueOf 8L) + if r = BigInteger.Three || r = (BigInteger.ValueOf 5L) then + innerLoop n -t + else + innerLoop n t + let n, t = innerLoop n t + + if k.Mod BigInteger.Four = BigInteger.Three + && n.Mod BigInteger.Four = BigInteger.Three then + loop (k.Mod n) n -t + else + loop (k.Mod n) n t + + let _, k, t = loop n k 1 + + if k = BigInteger.One then + t + else + 0 + +// should be equivalent to https://github.com/litecoin-project/litecoin/blob/master/src/secp256k1-zkp/src/field_impl.h#L290 +let IsQuadVar (elem: ECFieldElement) = + if isNull elem then + false + else + Jakobi elem >= 0 + +let SchnorrSign (key: array) (msgHash: array) : Signature = + let numBytesInSha256 = 32 + let k0 = + let hasher = Sha256Digest() + hasher.BlockUpdate(key, 0, key.Length) + hasher.BlockUpdate(msgHash, 0, msgHash.Length) + let arr = Array.zeroCreate numBytesInSha256 + hasher.DoFinal(arr, 0) |> ignore + BigInteger.FromByteArrayUnsigned(arr).Mod(scalarOrder) + + if k0 = BigInteger.Zero then + failwith "Failure. This happens only with negligible probability." + + let keyScalar = BigInteger.FromByteArrayUnsigned key + Fsdk.Misc.BetterAssert (keyScalar < scalarOrder) "key is not in range [0; scalarOrder)" + + let R = generatorG.Multiply(k0).Normalize() + let k = if Jakobi R.AffineYCoord <> 1 then scalarOrder.Subtract k0 else k0 + let e = + let hasher = Sha256Digest() + let xEncoded = R.AffineXCoord.GetEncoded() + hasher.BlockUpdate(xEncoded, 0, xEncoded.Length) + let keyScalarTimesGEncoded = generatorG.Multiply(keyScalar).GetEncoded(true) + hasher.BlockUpdate(keyScalarTimesGEncoded, 0, keyScalarTimesGEncoded.Length) + hasher.BlockUpdate(msgHash, 0, msgHash.Length) + let arr = Array.zeroCreate numBytesInSha256 + hasher.DoFinal(arr, 0) |> ignore + BigInteger.FromByteArrayUnsigned(arr).Mod(scalarOrder) + + Array.append + (R.AffineXCoord.GetEncoded()) + (k.Add(e.Multiply(keyScalar)).Mod(scalarOrder).ToUInt256().ToBytes()) + |> BigInt + |> Signature diff --git a/src/NLitecoin/MimbleWimble/Pedersen.fs b/src/NLitecoin/MimbleWimble/Pedersen.fs new file mode 100644 index 0000000..408781d --- /dev/null +++ b/src/NLitecoin/MimbleWimble/Pedersen.fs @@ -0,0 +1,83 @@ +module NLitecoin.MimbleWimble.Pedersen + +open Org.BouncyCastle.Crypto.Digests +open Org.BouncyCastle.Crypto.Parameters +open Org.BouncyCastle.Asn1.X9 +open Org.BouncyCastle.Math +open NBitcoin + +open EC + +// https://github.com/litecoin-project/litecoin/blob/5ac781487cc9589131437b23c69829f04002b97e/src/secp256k1-zkp/src/modules/commitment/main_impl.h#L41 +let SerializeCommitment (commitment: ECPoint) = + let bytes = + if commitment.IsInfinity || (commitment.XCoord.IsZero && commitment.YCoord.IsZero) then + Array.zeroCreate PedersenCommitment.NumBytes + else + commitment.GetEncoded true + bytes.[0] <- 9uy ^^^ (if EC.IsQuadVar (commitment.Normalize().YCoord) then 1uy else 0uy) + bytes + +let DeserializeCommitment (commitment: PedersenCommitment) : ECPoint = + let x = commitment.ToBytes() |> Array.skip 1 |> BigInteger.FromByteArrayUnsigned |> curve.Curve.FromBigInteger + let y = x.Square().Multiply(x).Add(curve.Curve.B).Sqrt() + let point = curve.Curve.CreatePoint(x.ToBigInteger(), y.ToBigInteger()) + if commitment.ToBytes().[0] &&& 1uy <> 0uy then + point.Negate() + else + point + +/// Generates a pedersen commitment: *commit = blind * G + value * H. The blinding factor is 32 bytes. +let Commit (value: Amount) (blind: BlindingFactor) : PedersenCommitment = + let result = + let blind = blind.ToUInt256().ToBytes() |> BigInteger.FromByteArrayUnsigned + let a = generatorG.Multiply(blind) + let b = generatorH.Multiply(BigInteger.ValueOf value) + a.Add b + let bytes = SerializeCommitment result + assert(bytes.Length = PedersenCommitment.NumBytes) + PedersenCommitment(BigInt bytes) + +/// Calculates the blinding factor x' = x + SHA256(xG+vH | xJ), used in the switch commitment x'G+vH. +let BlindSwitch (blindingFactor: BlindingFactor) (amount: Amount) : BlindingFactor = + let hasher = Sha256Digest() + + let x = blindingFactor.ToUInt256().ToBytes() |> BigInteger.FromByteArrayUnsigned + /// xG + vH + let commit = Commit amount blindingFactor + let commitSerialized = match commit with | PedersenCommitment num -> num.Data + hasher.BlockUpdate(commitSerialized, 0, commitSerialized.Length) + + // xJ + let xJ = generatorJPub.Multiply(x) + let xJSerialized = xJ.GetEncoded true + hasher.BlockUpdate(xJSerialized, 0, xJSerialized.Length) + + let hash = Array.zeroCreate 32 + hasher.DoFinal(hash, 0) |> ignore + + let result = x.Add((hash |> BigInteger.FromByteArrayUnsigned).Mod(EC.curve.Curve.Field.Characteristic)).Mod(scalarOrder) + + result.ToUInt256() + |> BlindingFactor + +let AddBlindingFactors (positive: array) (negative: array) : BlindingFactor = + let sum (factors: array) = + factors + |> Array.map (fun blind -> blind.ToUInt256().ToBytes() |> BigInteger.FromByteArrayUnsigned) + |> Array.fold (fun (a : BigInteger) b -> a.Add b) BigInteger.Zero + + let result = (sum positive).Subtract(sum negative).Mod(scalarOrder) + + result.ToUInt256() + |> BlindingFactor + +let AddCommitments (positive: array) (negative: array) : PedersenCommitment = + let sum (commitments: array) = + commitments + |> Array.map DeserializeCommitment + |> Array.fold (fun (a : ECPoint) b -> a.Add b) (generatorG.Multiply BigInteger.Zero) + + let result = (sum positive).Subtract(sum negative) + + result |> SerializeCommitment |> BigInt |> PedersenCommitment diff --git a/src/NLitecoin/MimbleWimble/TransactionBuilder.fs b/src/NLitecoin/MimbleWimble/TransactionBuilder.fs new file mode 100644 index 0000000..5e0df38 --- /dev/null +++ b/src/NLitecoin/MimbleWimble/TransactionBuilder.fs @@ -0,0 +1,374 @@ +module NLitecoin.MimbleWimble.TransactionBuilder + +open System + +open NBitcoin +open Org.BouncyCastle.Math + +open EC + +type Coin = NLitecoin.MimbleWimble.Coin +type Transaction = NLitecoin.MimbleWimble.Transaction + +exception IncorrectBalanceException of string + +type private Inputs = + { + TotalBlind: BlindingFactor + TotalKey: uint256 + Inputs: array + } + +type private Outputs = + { + TotalBlind: BlindingFactor + TotalKey: uint256 + Outputs: array + Coins: array + } + +type TransactionBuildResult = + { + Transaction: Transaction + OutputCoins: array + } + +/// Creates a standard input with a stealth key (feature bit = 1) +let private CreateInput + (outputId: Hash) + (commitment: PedersenCommitment) + (inputKey: Secp256k1.ECPrivKey) + (outputKey: Secp256k1.ECPrivKey) = + let features = InputFeatures.STEALTH_KEY_FEATURE_BIT + + let inputPubKey = PublicKey(inputKey.CreatePubKey().ToBytes() |> BigInt) + let outputPubKey = PublicKey(outputKey.CreatePubKey().ToBytes() |> BigInt) + + // Hash keys (K_i||K_o) + let keyHasher = Hasher() + keyHasher.Append inputPubKey + keyHasher.Append outputPubKey + let keyHash = keyHasher.Hash().ToBytes() + + // Calculate aggregated key k_agg = k_i + HASH(K_i||K_o) * k_o + let sigKey = + outputKey + .TweakMul(keyHash) + .TweakAdd(inputKey.ToBytes()) + + let msgHasher = Hasher() + msgHasher.Write (features |> uint8 |> Array.singleton) + msgHasher.Append outputId + let msgHash = msgHasher.Hash().ToBytes() + + let schnorrSignature = SchnorrSign (sigKey.ToBytes()) msgHash + + { + Features = features + OutputID = outputId + Commitment = commitment + InputPublicKey = Some inputPubKey + OutputPublicKey = outputPubKey + Signature = schnorrSignature + ExtraData = Array.empty + } + +let private CreateInputs (inputCoins: seq) : Inputs = + let blinds, keys, inputs = + [| for inputCoin in inputCoins do + let blind = Pedersen.BlindSwitch inputCoin.Blind.Value inputCoin.Amount + let ephemeralKey = NBitcoin.RandomUtils.GetUInt256() + let input = + CreateInput + inputCoin.OutputId + (Pedersen.Commit inputCoin.Amount blind) + (Secp256k1.ECPrivKey.Create(ephemeralKey.ToBytes())) + (Secp256k1.ECPrivKey.Create(inputCoin.SpendKey.Value.ToBytes())) + yield blind, (BlindingFactor ephemeralKey, BlindingFactor inputCoin.SpendKey.Value), input |] + |> Array.unzip3 + + let positiveKeys, negativeKeys = Array.unzip keys + + { + TotalBlind = Pedersen.AddBlindingFactors blinds Array.empty + TotalKey = (Pedersen.AddBlindingFactors positiveKeys negativeKeys).ToUInt256() + Inputs = inputs + } + +let private CreateOutput (senderPrivKey: uint256) (receiverAddr: StealthAddress) (value: uint64) : Output * BlindingFactor = + let features = OutputFeatures.STANDARD_FIELDS_FEATURE_BIT + + // Generate 128-bit secret nonce 'n' = Hash128(T_nonce, sender_privkey) + let n = + let hasher = Hasher(HashTags.NONCE) + hasher.Write(senderPrivKey.ToBytes()) + hasher.Hash().ToBytes() + |> Array.take 16 + |> BigInt + + // Calculate unique sending key 's' = H(T_send, A, B, v, n) + let s = + let hasher = Hasher(HashTags.SEND_KEY) + hasher.Append receiverAddr.ScanPubKey + hasher.Append receiverAddr.SpendPubKey + hasher.Write (BitConverter.GetBytes value) + hasher.Append n + hasher.Hash().ToBytes() |> Secp256k1.ECPrivKey.Create + + let A = + match receiverAddr.ScanPubKey with + | PublicKey pubKey -> Secp256k1.ECPubKey.Create pubKey.Data + + let B = + match receiverAddr.SpendPubKey with + | PublicKey pubKey -> Secp256k1.ECPubKey.Create pubKey.Data + + // Derive shared secret 't' = H(T_derive, s*A) + let sA = A.TweakMul(s.ToBytes()) + let t = + let hasher = Hasher(HashTags.DERIVE) + hasher.Write(sA.ToBytes()) + hasher.Hash() + + // Construct one-time public key for receiver 'Ko' = H(T_outkey, t)*B + let Ko = + let hasher = Hasher(HashTags.OUT_KEY) + hasher.Append t + B.TweakMul(hasher.Hash().ToBytes()) + + // Key exchange public key 'Ke' = s*B + let Ke = B.TweakMul(s.ToBytes()) + + // Calc blinding factor and mask nonce and amount. + let mask = OutputMask.FromShared(t.ToUInt256()) + let blind = Pedersen.BlindSwitch mask.PreBlind (int64 value) + let mv = mask.MaskValue value + let mn = mask.MaskNonce n + + // Commitment 'C' = r*G + v*H + let outputCommit = Pedersen.Commit (int64 value) blind + + // Calculate the ephemeral send pubkey 'Ks' = ks*G + let Ks = Secp256k1.ECPrivKey.Create(senderPrivKey.ToBytes()).CreatePubKey() + + // Derive view tag as first byte of H(T_tag, sA) + let viewTag = + let hasher = Hasher(HashTags.TAG) + hasher.Write(sA.ToBytes()) + hasher.Hash().ToBytes().[0] + + let message = + let keyExchangePubKey = + let bytes = Ke.ToBytes true + assert(bytes.Length = PublicKey.NumBytes) + bytes |> BigInt |> PublicKey + { + Features = features + StandardFields = + Some { + KeyExchangePubkey = keyExchangePubKey + ViewTag = viewTag + MaskedValue = mv + MaskedNonce = mn + } + ExtraData = Array.empty + } + + let rangeProof = + let emptyProofMessage = Array.zeroCreate 20 + + let messageSerialized = + use memoryStream = new System.IO.MemoryStream() + let stream = new BitcoinStream(memoryStream, true) + (message :> ISerializeable).Write stream + memoryStream.ToArray() + + Bulletproof.ConstructRangeProof + value + (blind.ToUInt256()) + (NBitcoin.RandomUtils.GetUInt256()) + (NBitcoin.RandomUtils.GetUInt256()) + emptyProofMessage + (Some messageSerialized) + + // Sign the output + let signature = + let hasher = Hasher() + hasher.Append outputCommit + hasher.Write (Ks.ToBytes true) + hasher.Write (Ko.ToBytes true) + hasher.Write (Hasher.CalculateHash(message).ToBytes()) + hasher.Write (Hasher.CalculateHash(rangeProof).ToBytes()) + let sigMessage = hasher.Hash() + SchnorrSign (senderPrivKey.ToBytes()) (sigMessage.ToBytes()) + + let blindOut = mask.PreBlind + + let output = + { + Commitment = outputCommit + SenderPublicKey = PublicKey(Ks.ToBytes true |> BigInt) + ReceiverPublicKey = PublicKey(Ko.ToBytes true |> BigInt) + Message = message + RangeProof = rangeProof + Signature = signature + } + + output, blindOut + +let private CreateOutputs (recipients: seq) : Outputs = + let outputBlinds, outputs, coins = + [| for recipient in recipients do + let ephemeralKey = NBitcoin.RandomUtils.GetUInt256() + let output, rawBlind = CreateOutput ephemeralKey recipient.Address (uint64 recipient.Amount) + let outputBlind = Pedersen.BlindSwitch rawBlind recipient.Amount + let coin = + { Coin.Empty with + Blind = Some rawBlind + Amount = recipient.Amount + OutputId = output.GetOutputID() + SenderKey = Some ephemeralKey + Address = Some recipient.Address + } + yield outputBlind, output, coin + |] + |> Array.unzip3 + + let outputKeys = + coins + |> Array.choose (fun coin -> coin.SenderKey) + |> Array.map BlindingFactor + + { + TotalBlind = Pedersen.AddBlindingFactors outputBlinds Array.empty + TotalKey = (Pedersen.AddBlindingFactors outputKeys Array.empty).ToUInt256() + Outputs = outputs + Coins = coins + } + +let private CreateKernel + (blind: BlindingFactor) + (stealthBlind: BlindingFactor) + (fee: Amount) + (peginAmount: Option) + (pegouts: array) + : Kernel = + let featuresByte = + (if fee > 0L then + KernelFeatures.FEE_FEATURE_BIT + else + enum 0) + ||| (match peginAmount with + | Some value when value > 0L -> KernelFeatures.PEGIN_FEATURE_BIT + | _ -> enum 0) + ||| (if pegouts.Length > 0 then + KernelFeatures.PEGOUT_FEATURE_BIT + else + enum 0) + ||| KernelFeatures.STEALTH_EXCESS_FEATURE_BIT + + let excessCommit = Pedersen.Commit 0L blind + + let stealthExcess = stealthBlind.ToUInt256().ToBytes() + + let sigKey = + let hasher = Hasher() + hasher.Append excessCommit + hasher.Write stealthExcess + NBitcoin.Secp256k1.ECPrivKey.Create(blind.ToUInt256().ToBytes()) + .TweakMul(hasher.Hash().ToBytes()) + .TweakAdd(stealthBlind.ToUInt256().ToBytes()) + + let sigMessage = + use byteStream = new IO.MemoryStream() + let stream = BitcoinStream(byteStream, true) + + stream.ReadWrite (featuresByte |> uint8) |> ignore + Helpers.Write stream excessCommit + stream.ReadWriteAsVarInt (fee |> uint64 |> ref) + match peginAmount with + | Some amount -> stream.ReadWriteAsVarInt (amount |> uint64 |> ref) + | None -> () + if pegouts.Length > 0 then + Helpers.WriteArray stream pegouts + Helpers.Write stream (BigInt stealthExcess) + + let hasher = Hasher() + hasher.Write(byteStream.ToArray()) + hasher.Hash() + + let signature = + let sigKeyBytes = Array.zeroCreate 32 + sigKey.WriteToSpan sigKeyBytes + SchnorrSign sigKeyBytes (sigMessage.ToBytes()) + + { + Features = featuresByte + Fee = Some fee + Pegin = peginAmount + Pegouts = pegouts + LockHeight = None + StealthExcess = stealthExcess |> BigInt |> PublicKey |> Some + Excess = excessCommit + Signature = signature + ExtraData = Array.empty + } + +let BuildTransaction + (inputCoins: array) + (recipients: array) + (pegouts: array) + (peginAmount: Option) + (fee: Amount) + : TransactionBuildResult = + let pegoutTotal = pegouts |> Array.sumBy (fun pegout -> pegout.Amount) + let recipientTotal = recipients |> Array.sumBy (fun recipient -> recipient.Amount) + let inputTotal = inputCoins |> Array.sumBy (fun coin -> coin.Amount) + + if inputTotal + (peginAmount |> Option.defaultValue 0L) <> pegoutTotal + recipientTotal + fee then + let msg = + "Incorrect balance: " + + (sprintf "inputTotal(%d) + peginAmount(%A) <> pegoutTotal(%d) + recipientTotal(%d) + fee(%d)" + inputTotal + peginAmount + pegoutTotal + recipientTotal + fee) + raise <| IncorrectBalanceException msg + + let inputs = CreateInputs inputCoins + let outputs = CreateOutputs recipients + + // Total kernel offset is split between raw kernel_offset and the kernel's blinding factor. + // sum(output.blind) - sum(input.blind) = kernel_offset + sum(kernel.blind) + let kernelOffset = NBitcoin.RandomUtils.GetUInt256() |> BlindingFactor + let kernelBlind = + Pedersen.AddBlindingFactors + [| outputs.TotalBlind |] + [| inputs.TotalBlind; kernelOffset |] + + let stealthBlind = NBitcoin.RandomUtils.GetUInt256() |> BlindingFactor + + let kernel = CreateKernel kernelBlind stealthBlind fee peginAmount pegouts + + let stealthOffset = + Pedersen.AddBlindingFactors + [| BlindingFactor outputs.TotalKey; BlindingFactor inputs.TotalKey |] + [| stealthBlind |] + + let transaction = + { + KernelOffset = kernelOffset + StealthOffset = stealthOffset + Body = + { + Inputs = inputs.Inputs |> Array.sort + Outputs = outputs.Outputs |> Array.sort + Kernels = Array.singleton kernel + } + } + + { + Transaction = transaction + OutputCoins = outputs.Coins + } diff --git a/src/NLitecoin/MimbleWimble/Types.fs b/src/NLitecoin/MimbleWimble/Types.fs new file mode 100644 index 0000000..1748028 --- /dev/null +++ b/src/NLitecoin/MimbleWimble/Types.fs @@ -0,0 +1,777 @@ +namespace NLitecoin.MimbleWimble + +open System +open System.IO + +open Fsdk.Misc +open NBitcoin +open NBitcoin.Protocol +open Org.BouncyCastle.Crypto.Digests +open Org.BouncyCastle.Crypto.Parameters + +type ISerializeable = + abstract Write: BitcoinStream -> unit + // no Read() method as it will be static method and can't be included in interface + +/// Amount of litecoins in litoshi +type Amount = int64 + +[] +module Helpers = + let Write (stream: BitcoinStream) (object: #ISerializeable) = + BetterAssert stream.Serializing "stream.Serializing should be true when writing" + object.Write stream + + let WriteUint256 (stream: BitcoinStream) (number: uint256) = + BetterAssert stream.Serializing "stream.Serializing should be true when writing" + number.AsBitcoinSerializable().ReadWrite stream + + let ReadUint256 (stream: BitcoinStream) : uint256 = + BetterAssert (not stream.Serializing) "stream.Serializing should be false when reading" + let tempValue = uint256.MutableUint256() + tempValue.ReadWrite stream + tempValue.Value + + let ReadArray<'T> (stream: BitcoinStream) (readFunc : BitcoinStream -> 'T) : array<'T> = + let len = int <| VarInt.StaticRead stream + Array.init len (fun _ -> readFunc stream) + + let WriteArray<'T when 'T :> ISerializeable> (stream: BitcoinStream) (arr: array<'T>) = + let len = uint64 arr.Length + VarInt.StaticWrite(stream, len) + for each in arr do + each.Write stream + + let ReadByteArray (stream: BitcoinStream) : array = + ReadArray + stream + (fun s -> + s.ReadWrite Unchecked.defaultof) + + let WriteByteArray (stream: BitcoinStream) (arr: array) = + let len = uint64 arr.Length + VarInt.StaticWrite(stream, len) + stream.ReadWrite arr + + let ReadAmount (stream: BitcoinStream) : Amount = + let amountRef = ref 0UL + stream.ReadWriteAsCompactVarInt amountRef + amountRef.Value |> int64 + + let WriteAmount (stream: BitcoinStream) (amount: Amount) = + stream.ReadWriteAsCompactVarInt(amount |> uint64 |> ref) + + let ConvertBits (fromBits: int) (toBits: int) (input: array) : array = + let maxV = (1u <<< toBits) - 1u + let maxAcc = (1u <<< (fromBits + toBits - 1)) - 1u + [| + let mutable acc = 0u + let mutable bits = 0 + for currByte in input do + acc <- ((acc <<< fromBits) ||| (uint32 currByte)) &&& maxAcc + bits <- bits + fromBits + while bits >= toBits do + bits <- bits - toBits + yield byte((acc >>> bits) &&& maxV) + if bits > 0 then + yield byte((acc <<< (toBits - int bits)) &&& maxV) + |] + +type BlindingFactor = + | BlindingFactor of uint256 + member self.ToUInt256() = + match self with + | BlindingFactor number -> number + + static member Read(stream: BitcoinStream) : BlindingFactor = + BlindingFactor(ReadUint256 stream) + + interface ISerializeable with + member self.Write(stream) = + match self with + | BlindingFactor number -> number |> WriteUint256 stream + +type Hash = + | Hash of uint256 + member self.ToUInt256() = + match self with + | Hash number -> number + + member self.ToBytes() = + self.ToUInt256().ToBytes() + + static member Read(stream: BitcoinStream) : Hash = + ReadUint256 stream |> Hash + + interface ISerializeable with + member self.Write(stream) = + match self with + | Hash number -> number |> WriteUint256 stream + +module internal HashTags = + let ADDRESS = 'A' + let BLIND = 'B' + let DERIVE = 'D' + let NONCE = 'N' + let OUT_KEY = 'O' + let SEND_KEY = 'S' + let TAG = 'T' + let NONCE_MASK = 'X' + let VALUE_MASK = 'Y' + +type Hasher(?hashTag: char) = + let blake3 = Blake3Digest() + do + blake3.Init(Blake3Parameters()) + match hashTag with + | Some tag -> blake3.Update(byte tag) + | None -> () + + member _.Write(bytes: array) = + blake3.BlockUpdate(bytes, 0, bytes.Length) + + member self.Append(object: ISerializeable) = + use stream = new MemoryStream() + let writer = new BitcoinStream(stream, true) + object.Write writer + self.Write(stream.ToArray()) + + member _.Hash() = + let length = 32 + let bytes = Array.zeroCreate length + blake3.OutputFinal(bytes, 0, length) |> ignore + Hash(uint256 bytes) + + static member CalculateHash(object: ISerializeable) = + let hasher = Hasher() + hasher.Append object + hasher.Hash() + +type BigInt(bytes: array) = + member _.Data = bytes + + interface IEquatable with + override self.Equals other = self.Data = other.Data + + override self.Equals other = + match other with + | :? BigInt as otherBigInt -> self.Data = otherBigInt.Data + | _ -> false + + override self.GetHashCode() = self.Data.GetHashCode() + + interface IComparable with + override self.CompareTo other = + match other with + | :? BigInt as otherBigInt -> + compare self.Data otherBigInt.Data + | _ -> failwith "Other is not BigInt" + + override self.ToString() = + let encoder = NBitcoin.DataEncoders.HexEncoder() + sprintf "BigInt %s" (encoder.EncodeData bytes) + + static member Read(stream: BitcoinStream) (numBytes: int) : BigInt = + BetterAssert (not stream.Serializing) "stream.Serializing should be false when reading" + let result : array = Array.zeroCreate numBytes + stream.ReadWrite result + BigInt result + + interface ISerializeable with + member self.Write(stream) = + BetterAssert stream.Serializing "stream.Serializing should be true when writing" + stream.ReadWrite self.Data + +type PedersenCommitment = + | PedersenCommitment of BigInt + static member NumBytes = 33 + + static member Read(stream: BitcoinStream) = + PedersenCommitment(BigInt.Read stream PedersenCommitment.NumBytes) + + member self.ToBytes() = + match self with + | PedersenCommitment bigint -> bigint.Data + + interface ISerializeable with + member self.Write(stream) = + match self with + | PedersenCommitment number -> (number :> ISerializeable).Write stream + +type PublicKey = + | PublicKey of BigInt + static member NumBytes = 33 + + static member Read(stream: BitcoinStream) = + PublicKey(BigInt.Read stream PublicKey.NumBytes) + + interface ISerializeable with + member self.Write(stream) = + match self with + | PublicKey number -> (number :> ISerializeable).Write stream + + member self.ToBytes() = + match self with + | PublicKey bigint -> bigint.Data + +type Signature = + | Signature of BigInt + static member NumBytes = 64 + + static member Read(stream: BitcoinStream) = + Signature(BigInt.Read stream Signature.NumBytes) + + interface ISerializeable with + member self.Write(stream) = + match self with + | Signature number -> (number :> ISerializeable).Write stream + +type InputFeatures = + | STEALTH_KEY_FEATURE_BIT = 0x01 + | EXTRA_DATA_FEATURE_BIT = 0x02 + +type OutputFeatures = + | STANDARD_FIELDS_FEATURE_BIT = 0x01 + | EXTRA_DATA_FEATURE_BIT = 0x02 + +[] +type Input = + { + Features: InputFeatures + OutputID: Hash + Commitment: PedersenCommitment + InputPublicKey: Option + OutputPublicKey: PublicKey + ExtraData: array + Signature: Signature + } + + static member Read(stream: BitcoinStream) : Input = + BetterAssert (not stream.Serializing) "stream.Serializing should be false when reading" + let features = (stream.ReadWrite Unchecked.defaultof) |> int |> enum + let outputId = Hash.Read stream + let commitment = PedersenCommitment.Read stream + let outputPubKey = PublicKey.Read stream + + let inputPubKey = + if int(features &&& InputFeatures.STEALTH_KEY_FEATURE_BIT) <> 0 then + Some <| PublicKey.Read stream + else + None + + let extraData = + if int(features &&& InputFeatures.EXTRA_DATA_FEATURE_BIT) <> 0 then + ReadByteArray stream + else + Array.empty + + let signature = Signature.Read stream + + let result = + { + Features = features + OutputID = outputId + Commitment = commitment + OutputPublicKey = outputPubKey + InputPublicKey = inputPubKey + ExtraData = extraData + Signature = signature + } + + result + + interface ISerializeable with + member self.Write(stream) = + BetterAssert stream.Serializing "stream.Serializing should be true when writing" + + stream.ReadWrite (byte self.Features) |> ignore + Write stream self.OutputID + Write stream self.Commitment + Write stream self.OutputPublicKey + + if int(self.Features &&& InputFeatures.STEALTH_KEY_FEATURE_BIT) <> 0 then + Write stream self.InputPublicKey.Value + + if int(self.Features &&& InputFeatures.EXTRA_DATA_FEATURE_BIT) <> 0 then + WriteByteArray stream self.ExtraData + + Write stream self.Signature + + interface IComparable with + member self.CompareTo(other) = + compare (Hasher.CalculateHash self) (Hasher.CalculateHash other) + + interface IComparable with + member self.CompareTo(other) = + match other with + | :? Input as input -> (self :> IComparable).CompareTo input + | _ -> 0 + +type OutputMessageStandardFields = + { + KeyExchangePubkey: PublicKey + ViewTag: uint8 + MaskedValue: uint64 + MaskedNonce: BigInt + } + static member MaskedNonceNumBytes = 16 + +type OutputMessage = + { + Features: OutputFeatures + StandardFields: Option + ExtraData: array + } + static member Read(stream: BitcoinStream) : OutputMessage = + BetterAssert (not stream.Serializing) "stream.Serializing should be false when reading" + + let featuresByte = stream.ReadWrite Unchecked.defaultof + let features = featuresByte |> int |> enum + + let standardFields = + if int(features &&& OutputFeatures.STANDARD_FIELDS_FEATURE_BIT) <> 0 then + let pubKey = PublicKey.Read stream + let viewTag = stream.ReadWrite Unchecked.defaultof + let maskedValue = stream.ReadWrite Unchecked.defaultof + let maskedNonce = BigInt.Read stream OutputMessageStandardFields.MaskedNonceNumBytes + { + KeyExchangePubkey = pubKey + ViewTag = viewTag + MaskedValue = maskedValue + MaskedNonce = maskedNonce + } + |> Some + else + None + + let extraData = + if int(features &&& OutputFeatures.EXTRA_DATA_FEATURE_BIT) <> 0 then + ReadByteArray stream + else + Array.empty + + { + Features = features + StandardFields = standardFields + ExtraData = extraData + } + + interface ISerializeable with + member self.Write(stream) = + BetterAssert stream.Serializing "stream.Serializing should be true when writing" + + stream.ReadWrite(self.Features |> uint8) |> ignore + + if int(self.Features &&& OutputFeatures.STANDARD_FIELDS_FEATURE_BIT) <> 0 then + let fields = self.StandardFields.Value + Write stream fields.KeyExchangePubkey + stream.ReadWrite fields.ViewTag |> ignore + stream.ReadWrite fields.MaskedValue |> ignore + (fields.MaskedNonce :> ISerializeable).Write stream + + if int(self.Features &&& OutputFeatures.EXTRA_DATA_FEATURE_BIT) <> 0 then + WriteByteArray stream self.ExtraData + +type RangeProof = + | RangeProof of array + + static member Size = 675 + + static member Read(stream: BitcoinStream) : RangeProof = + BetterAssert (not stream.Serializing) "stream.Serializing should be false when reading" + let bytes = Array.zeroCreate RangeProof.Size + stream.ReadWrite bytes + RangeProof bytes + + interface ISerializeable with + member self.Write(stream) = + BetterAssert stream.Serializing "stream.Serializing should be true when writing" + match self with + | RangeProof bytes -> + BetterAssert (bytes.Length = RangeProof.Size) "incorrect proof size" + stream.ReadWrite bytes + +type StealthAddress = + { + ScanPubKey: PublicKey + SpendPubKey: PublicKey + } + interface ISerializeable with + member self.Write(stream) = + (self.ScanPubKey :> ISerializeable).Write stream + (self.SpendPubKey :> ISerializeable).Write stream + + static member Bech32Prefix = "ltcmweb" + + static member DecodeDestination(addressString: string) : StealthAddress = + let encoder = + DataEncoders.Encoders.Bech32(StealthAddress.Bech32Prefix, StrictLength = false) + let bytes = + encoder + .DecodeDataRaw(addressString, ref DataEncoders.Bech32EncodingType.BECH32) + use memoryStream = + let bitsInByte = 8 + let bitsExpectedByBech32Encoder = 5 + new MemoryStream(bytes |> Array.skip 1 |> ConvertBits bitsExpectedByBech32Encoder bitsInByte) + let bitcoinStream = new BitcoinStream(memoryStream, false) + { + ScanPubKey = PublicKey.Read bitcoinStream + SpendPubKey = PublicKey.Read bitcoinStream + } + + member self.EncodeDestination() : string = + use memoryStream = new MemoryStream() + let bitcoinStream = new BitcoinStream(memoryStream, true) + (self :> ISerializeable).Write bitcoinStream + let data = + let bitsInByte = 8 + let bitsExpectedByBech32Encoder = 5 + Array.append + (Array.singleton 0uy) + (memoryStream.ToArray() |> ConvertBits bitsInByte bitsExpectedByBech32Encoder) + + DataEncoders.Encoders.Bech32(StealthAddress.Bech32Prefix) + .EncodeData(data, DataEncoders.Bech32EncodingType.BECH32) + +type OutputMask = + { + PreBlind: BlindingFactor + ValueMask: uint64 + NonceMask: BigInt + } + static member NonceMaskNumBytes = 16 + + /// Feeds the shared secret 't' into tagged hash functions to derive: + /// q - the blinding factor + /// v' - the value mask + /// n' - the nonce mask + static member FromShared (sharedSecret: uint256) = + let preBlind = + let hasher = Hasher(HashTags.BLIND) + hasher.Write(sharedSecret.ToBytes()) + hasher.Hash().ToUInt256() + |> BlindingFactor.BlindingFactor + let valueMask = + let hasher = Hasher(HashTags.VALUE_MASK) + hasher.Write(sharedSecret.ToBytes()) + hasher.Hash().ToBytes() + |> Array.take 8 + |> BitConverter.ToUInt64 + let nonceMask = + let hasher = Hasher(HashTags.NONCE_MASK) + hasher.Write(sharedSecret.ToBytes()) + hasher.Hash().ToBytes() + |> Array.take OutputMask.NonceMaskNumBytes + |> BigInt + { + PreBlind = preBlind + ValueMask = valueMask + NonceMask = nonceMask + } + + member self.MaskValue (value: uint64) = + value ^^^ self.ValueMask + + member self.MaskNonce (nonce: BigInt) = + Array.map2 + (^^^) + nonce.Data + self.NonceMask.Data + |> BigInt + +[] +type Output = + { + Commitment: PedersenCommitment + SenderPublicKey: PublicKey + ReceiverPublicKey: PublicKey + Message: OutputMessage + RangeProof: RangeProof + Signature: Signature + } + static member Read(stream: BitcoinStream) : Output = + BetterAssert (not stream.Serializing) "stream.Serializing should be false when reading" + { + Commitment = PedersenCommitment.Read stream + SenderPublicKey = PublicKey.Read stream + ReceiverPublicKey = PublicKey.Read stream + Message = OutputMessage.Read stream + RangeProof = RangeProof.Read stream + Signature = Signature.Read stream + } + + interface ISerializeable with + member self.Write stream = + BetterAssert stream.Serializing "stream.Serializing should be true when writing" + + Write stream self.Commitment + Write stream self.SenderPublicKey + Write stream self.ReceiverPublicKey + Write stream self.Message + Write stream self.RangeProof + Write stream self.Signature + + interface IComparable with + member self.CompareTo other = + compare (Hasher.CalculateHash self) (Hasher.CalculateHash other) + + interface IComparable with + member self.CompareTo other = + match other with + | :? Output as output -> (self :> IComparable).CompareTo output + | _ -> 0 + + member self.GetOutputID() : Hash = + let hasher = Hasher() + hasher.Append self.Commitment + hasher.Append self.SenderPublicKey + hasher.Append self.ReceiverPublicKey + hasher.Append(Hasher.CalculateHash self.Message) + hasher.Append(Hasher.CalculateHash self.RangeProof) + hasher.Append self.Signature + hasher.Hash() + +type KernelFeatures = + | FEE_FEATURE_BIT = 0x01 + | PEGIN_FEATURE_BIT = 0x02 + | PEGOUT_FEATURE_BIT = 0x04 + | HEIGHT_LOCK_FEATURE_BIT = 0x08 + | STEALTH_EXCESS_FEATURE_BIT = 0x10 + | EXTRA_DATA_FEATURE_BIT = 0x20 + +module KernelFeatures = + let ALL_FEATURE_BITS = + KernelFeatures.FEE_FEATURE_BIT ||| + KernelFeatures.PEGIN_FEATURE_BIT ||| + KernelFeatures.PEGOUT_FEATURE_BIT ||| + KernelFeatures.HEIGHT_LOCK_FEATURE_BIT ||| + KernelFeatures.STEALTH_EXCESS_FEATURE_BIT ||| + KernelFeatures.EXTRA_DATA_FEATURE_BIT + +type PegOutCoin = + { + Amount: Amount + ScriptPubKey: NBitcoin.Script // ? + } + static member Read(stream: BitcoinStream) : PegOutCoin = + BetterAssert (not stream.Serializing) "stream.Serializing should be false when reading" + let amount = ReadAmount stream + let scriptPubKeyRef = ref NBitcoin.Script.Empty + stream.ReadWrite scriptPubKeyRef + { + Amount = amount + ScriptPubKey = scriptPubKeyRef.Value + } + + interface ISerializeable with + member self.Write(stream) = + BetterAssert stream.Serializing "stream.Serializing should be true when writing" + WriteAmount stream self.Amount + stream.ReadWrite self.ScriptPubKey |> ignore + +[] +type Kernel = + { + Features: KernelFeatures + Fee: Option + Pegin: Option + Pegouts: array + LockHeight: Option + StealthExcess: Option + ExtraData: array + // Remainder of the sum of all transaction commitments. + // If the transaction is well formed, amounts components should sum to zero and the excess is hence a valid public key. + Excess: PedersenCommitment + // The signature proving the excess is a valid public key, which signs the transaction fee. + Signature: Signature + } + static member Read(stream: BitcoinStream) : Kernel = + BetterAssert (not stream.Serializing) "stream.Serializing should be false when reading" + let features = (stream.ReadWrite Unchecked.defaultof) |> int |> enum + + let fee = + if int(features &&& KernelFeatures.FEE_FEATURE_BIT) <> 0 then + Some <| ReadAmount stream + else + None + + let pegin = + if int(features &&& KernelFeatures.PEGIN_FEATURE_BIT) <> 0 then + Some <| ReadAmount stream + else + None + + let pegouts = + if int(features &&& KernelFeatures.PEGOUT_FEATURE_BIT) <> 0 then + ReadArray stream PegOutCoin.Read + else + Array.empty + + let lockHeight = + if int(features &&& KernelFeatures.HEIGHT_LOCK_FEATURE_BIT) <> 0 then + Some <| stream.ReadWrite Unchecked.defaultof + else + None + + let stealthExcess = + if int(features &&& KernelFeatures.STEALTH_EXCESS_FEATURE_BIT) <> 0 then + Some <| PublicKey.Read stream + else + None + + let extraData = + if int(features &&& KernelFeatures.EXTRA_DATA_FEATURE_BIT) <> 0 then + ReadByteArray stream + else + Array.empty + + let excess = PedersenCommitment.Read stream + let signature = Signature.Read stream + + { + Features = features + Fee = fee + Pegin = pegin + Pegouts = pegouts + LockHeight = lockHeight + StealthExcess = stealthExcess + ExtraData = extraData + Excess = excess + Signature = signature + } + + member self.GetSupplyChange() : Amount = + let pegOutAmount = self.Pegouts |> Array.sumBy (fun pegOut -> pegOut.Amount) + (self.Pegin |> Option.defaultValue 0L) - (self.Fee |> Option.defaultValue 0L) - pegOutAmount + + interface ISerializeable with + member self.Write(stream) = + BetterAssert stream.Serializing "stream.Serializing should be true when writing" + + stream.ReadWrite (self.Features |> byte) |> ignore + + if int(self.Features &&& KernelFeatures.FEE_FEATURE_BIT) <> 0 then + WriteAmount stream self.Fee.Value + + if int(self.Features &&& KernelFeatures.PEGIN_FEATURE_BIT) <> 0 then + WriteAmount stream self.Pegin.Value + + if int(self.Features &&& KernelFeatures.PEGOUT_FEATURE_BIT) <> 0 then + WriteArray stream self.Pegouts + + if int(self.Features &&& KernelFeatures.HEIGHT_LOCK_FEATURE_BIT) <> 0 then + stream.ReadWrite self.LockHeight.Value |> ignore + + if int(self.Features &&& KernelFeatures.STEALTH_EXCESS_FEATURE_BIT) <> 0 then + (self.StealthExcess.Value :> ISerializeable).Write stream + + if int(self.Features &&& KernelFeatures.EXTRA_DATA_FEATURE_BIT) <> 0 then + WriteByteArray stream self.ExtraData + + (self.Excess :> ISerializeable).Write stream + (self.Signature :> ISerializeable).Write stream + + interface IComparable with + member self.CompareTo(other) = + compare (Hasher.CalculateHash self) (Hasher.CalculateHash other) + + interface IComparable with + member self.CompareTo(other) = + match other with + | :? Kernel as kernel -> (self :> IComparable).CompareTo kernel + | _ -> 0 + +/// TRANSACTION BODY - Container for all inputs, outputs, and kernels in a transaction or block. +type TxBody = + { + /// List of inputs spent by the transaction. + Inputs: array + /// List of outputs the transaction produces. + Outputs: array + /// List of kernels that make up this transaction. + Kernels: array + } + static member Read(stream: BitcoinStream) : TxBody = + { + Inputs = ReadArray stream Input.Read + Outputs = ReadArray stream Output.Read + Kernels = ReadArray stream Kernel.Read + } + + interface ISerializeable with + member self.Write stream = + WriteArray stream self.Inputs + WriteArray stream self.Outputs + WriteArray stream self.Kernels + +type Transaction = + { + // The kernel "offset" k2 excess is k1G after splitting the key k = k1 + k2. + KernelOffset: BlindingFactor + StealthOffset: BlindingFactor + // The transaction body. + Body: TxBody + } + + /// Parse hex-encoded MimbleWimble transaction + static member ParseString(txString: string) : Transaction = + let encoder = NBitcoin.DataEncoders.HexEncoder() + let binaryTx = encoder.DecodeData txString + use memoryStream = new MemoryStream(binaryTx) + let bitcoinStream = new BitcoinStream(memoryStream, false) + let result = Transaction.Read bitcoinStream + result + + static member Read(stream: BitcoinStream) : Transaction = + { + KernelOffset = BlindingFactor.Read stream + StealthOffset = BlindingFactor.Read stream + Body = TxBody.Read stream + } + + interface ISerializeable with + member self.Write(stream) = + self.KernelOffset |> Write stream + self.StealthOffset |> Write stream + self.Body |> Write stream + +/// Represents an output owned by the wallet, and the keys necessary to spend it. +/// See https://github.com/litecoin-project/litecoin/blob/master/src/libmw/include/mw/models/wallet/Coin.h +type Coin = + { + AddressIndex: uint32 + SpendKey: Option + Blind: Option + Amount: Amount + OutputId: Hash + SenderKey: Option + Address: Option + SharedSecret: Option + } + static member ChangeIndex = 0u + static member PeginIndex = 1u + static member CustomKey = UInt32.MaxValue - 1u + static member UnknownIndex = UInt32.MaxValue + + member self.IsChange = self.AddressIndex = Coin.ChangeIndex + member self.IsPegIn = self.AddressIndex = Coin.PeginIndex + member self.IsMine = self.AddressIndex <> Coin.UnknownIndex + member self.HasSpendKey = self.SpendKey.IsSome + + static member Empty = + { + AddressIndex = Coin.UnknownIndex + SpendKey = None + Blind = None + Amount = 0L + OutputId = Hash(uint256 0UL) + SenderKey = None + Address = None + SharedSecret = None + } + +type Recipient = + { + Amount: Amount + Address: StealthAddress + } diff --git a/src/NLitecoin/MimbleWimble/Wallet.fs b/src/NLitecoin/MimbleWimble/Wallet.fs new file mode 100644 index 0000000..3780ece --- /dev/null +++ b/src/NLitecoin/MimbleWimble/Wallet.fs @@ -0,0 +1,379 @@ +module NLitecoin.MimbleWimble.Wallet + +open System + +open NBitcoin +open Org.BouncyCastle.Math + +open EC + +let KeyPurposeMweb = 100 + +type Coin = NLitecoin.MimbleWimble.Coin +type Transaction = NLitecoin.MimbleWimble.Transaction +type MutableDictionary<'K,'V> = Collections.Generic.Dictionary<'K,'V> + +type IKeyChain = + abstract GetStealthAddress: uint32 -> StealthAddress + abstract RewindOutput: Output -> Option + abstract PrivateScanKey: Key + +let RewindOutput + (keyChain: IKeyChain) + (getIndexForSpendKey: Secp256k1.ECPubKey -> Option) + (calculateOutputKey: uint256 -> uint32 -> Option) + (output: Output) : Option = + match output.Message.StandardFields with + | None -> None + | Some outputFields -> + let sharedSecret = + Secp256k1.ECPubKey.Create(outputFields.KeyExchangePubkey.ToBytes()) + .TweakMul(keyChain.PrivateScanKey.ToBytes()) + .ToBytes(true) + |> BigInt + |> PublicKey + let viewTag = + let hasher = Hasher HashTags.TAG + hasher.Append sharedSecret + hasher.Hash().ToBytes().[0] + if viewTag <> outputFields.ViewTag then + None + else + /// t + let ecdheSharedSecret = + let hasher = Hasher HashTags.DERIVE + hasher.Append sharedSecret + hasher.Hash() + + /// B_i + let spendPubKey = + let tHashed = + let hasher = Hasher HashTags.OUT_KEY + hasher.Append ecdheSharedSecret + hasher.Hash().ToBytes() |> BigInteger.FromByteArrayUnsigned + Secp256k1.ECPubKey.Create(output.ReceiverPublicKey.ToBytes()) + .TweakMul((tHashed.ModInverse EC.scalarOrder).ToByteArrayUnsigned()) + + match getIndexForSpendKey spendPubKey with + | None -> None + | Some index -> + let mask = OutputMask.FromShared(ecdheSharedSecret.ToUInt256()) + let value = mask.MaskValue outputFields.MaskedValue |> int64 + /// n + let maskNonce = mask.MaskNonce outputFields.MaskedNonce + + if Pedersen.Commit value (Pedersen.BlindSwitch mask.PreBlind value) <> output.Commitment then + None + else + let address = + { + SpendPubKey = spendPubKey.ToBytes true |> BigInt |> PublicKey + ScanPubKey = + spendPubKey.TweakMul(keyChain.PrivateScanKey.ToBytes()) + .ToBytes(true) + |> BigInt + |> PublicKey + } + // sending key 's' and check that s*B ?= Ke + let sendKey = + let hasher = Hasher HashTags.SEND_KEY + hasher.Append address.ScanPubKey + hasher.Append address.SpendPubKey + hasher.Write(BitConverter.GetBytes value) + hasher.Append maskNonce + hasher.Hash() + if outputFields.KeyExchangePubkey.ToBytes() <> + spendPubKey.TweakMul(sendKey.ToBytes()).ToBytes(true) then + None + else + { + AddressIndex = index + Blind = Some mask.PreBlind + Amount = value + OutputId = output.GetOutputID() + Address = Some address + SharedSecret = Some(ecdheSharedSecret.ToUInt256()) + SpendKey = calculateOutputKey (ecdheSharedSecret.ToUInt256()) index + SenderKey = None + } + |> Some + +let GetStealthAddress (spendPubKey: Secp256k1.ECPubKey) (privateScanKey: Key) : StealthAddress = + { + SpendPubKey = spendPubKey.ToBytes(true) |> BigInt |> PublicKey + ScanPubKey = spendPubKey.TweakMul(privateScanKey.ToBytes()).ToBytes(true) |> BigInt |> PublicKey + } + +type KeyChain(seed: array, maxUsedIndex: uint32) = + let masterKey = ExtKey.CreateFromSeed seed + // derive m/0' + let accountKey = masterKey.Derive(0, true) + // derive m/0'/100' (MWEB) + let chainChildKey = accountKey.Derive(KeyPurposeMweb, true) + + let scanKey = chainChildKey.Derive(0, true) + let spendKey = chainChildKey.Derive(1, true) + + let calculateSpendKey(index: uint32) : Secp256k1.ECPrivKey = + let mi = + let hasher = Hasher HashTags.ADDRESS + hasher.Write(BitConverter.GetBytes index) + hasher.Write(scanKey.PrivateKey.ToBytes()) + hasher.Hash() + Secp256k1.ECPrivKey.Create(spendKey.PrivateKey.ToBytes()).TweakAdd(mi.ToBytes()) + + // have to use dictionary as Secp256k1.ECPubKey doesn't implement comparison + let spendPubKeysMap = + MutableDictionary( + seq { + for i in 0u..maxUsedIndex -> + let spendPubKey = (calculateSpendKey i).CreatePubKey() + Collections.Generic.KeyValuePair(spendPubKey, i) }) + + static member DefaultInitialMaxUsedIndex = 100u + + new(seed: array) = KeyChain(seed, KeyChain.DefaultInitialMaxUsedIndex) + + interface IKeyChain with + override self.GetStealthAddress index = self.GetStealthAddress index + override self.RewindOutput output = self.RewindOutput output + override self.PrivateScanKey = self.ScanKey.PrivateKey + + member self.MaxUsedIndex : uint32 = spendPubKeysMap.Values |> Seq.max + + member self.ScanKey = scanKey + member self.SpendKey = spendKey + + member self.GetIndexForSpendKey(key: Secp256k1.ECPubKey) : Option = + match spendPubKeysMap.TryGetValue key with + | (true, value) -> Some value + | _ -> None + + member self.GetStealthAddress(index: uint32) : StealthAddress = + let spendPubKey = self.GetSpendKey(index) + GetStealthAddress spendPubKey scanKey.PrivateKey + + member self.GetSpendKey(index: uint32) : Secp256k1.ECPubKey = + match spendPubKeysMap |> Seq.tryFind (fun item -> item.Value = index) with + | Some item -> item.Key + | None -> + let spendKey = (calculateSpendKey index).CreatePubKey() + spendPubKeysMap.[spendKey] <- index + spendKey + + member self.RewindOutput(output: Output) : Option = + RewindOutput self self.GetIndexForSpendKey self.CalculateOutputKey output + + member private self.CalculateOutputKey (sharedSecret: uint256) (addressIndex: uint32) : Option = + if addressIndex = Coin.UnknownIndex || addressIndex = Coin.CustomKey then + None + else + let sharedSecretHashed = + let hasher = Hasher HashTags.OUT_KEY + hasher.Append(sharedSecret.ToBytes() |> BigInt) + hasher.Hash() + (calculateSpendKey addressIndex) + .TweakMul(sharedSecretHashed.ToBytes()) + .ToBytes() + |> uint256 + |> Some + +/// Keychain that doesn't have access to wallet seed, only to private scan key and public spend keys. +/// That is enough to check balance but not enough to spend funds. +type ReadonlyKeychain(privateScanKey: Key, spendPubKeysMap: Collections.Generic.IReadOnlyDictionary) = + interface IKeyChain with + override self.GetStealthAddress(index) = + let spendPubKey = (spendPubKeysMap |> Seq.find (fun kvPair -> kvPair.Value = index)).Key + GetStealthAddress spendPubKey privateScanKey + + override self.RewindOutput(output) = + let getIndexForPubKey pubKey = + match spendPubKeysMap.TryGetValue pubKey with + | (true, key) -> Some key + | (false, _) -> None + RewindOutput self getIndexForPubKey (fun _ _ -> None) output + + override self.PrivateScanKey = privateScanKey + +type Wallet(keyChain: IKeyChain, coins: Map, spentOutputs: Set) = + new(keyChain: IKeyChain) = Wallet(keyChain, Map.empty, Set.empty) + + member self.Coins = coins + member self.SpentOutputs = spentOutputs + + member self.CanSpend = keyChain :? KeyChain + + member self.AddCoin(coin: Coin) : Wallet = + Wallet(keyChain, coins |> Map.add coin.OutputId coin, spentOutputs) + + member self.GetCoin(outputId: Hash) : Option = + coins |> Map.tryFind outputId + + member self.MarkAsSpent(outputId: Hash) : Wallet = + Wallet(keyChain, coins, spentOutputs |> Set.add outputId) + + member self.GetUnspentCoins(): array = + [| + for outputId, coin in coins |> Map.toSeq do + if coin.IsMine && not(spentOutputs |> Set.contains outputId) then + yield coin + |] + + member self.GetBalance() : Amount = + self.GetUnspentCoins() |> Array.sumBy (fun coin -> coin.Amount) + + member self.RewindOutput(output: Output) : Wallet * Option = + match self.GetCoin(output.GetOutputID()) with + | Some coin when coin.IsMine -> + // If the coin has the spend key, it's fully rewound. If not, try rewinding further. + if coin.HasSpendKey then + self, Some coin + else + self, keyChain.RewindOutput output + | _ -> + match keyChain.RewindOutput output with + | Some coin -> self.AddCoin coin, Some coin + | None -> self, None + + /// Get our coins and spent outputs from transaction (if any) and update the wallet + member self.ProcessTransaction (transaction: Transaction) : Wallet = + let updatedWallet = + transaction.Body.Outputs + |> Array.fold + (fun (wallet: Wallet) output -> wallet.RewindOutput output |> fst) + self + + transaction.Body.Inputs + |> Array.fold + (fun (wallet: Wallet) input -> + if (wallet.GetCoin input.OutputID).IsSome then + wallet.MarkAsSpent input.OutputID + else + wallet) + updatedWallet + + /// For given amount, pick enough coins from available coins to cover the amount. + /// Create recipient from leftover amount if any. + member private self.GetInputCoinsAndChangeRecipient(totalAmount: Amount) : Option * Option> = + let coins = + self.GetUnspentCoins() + |> Array.sortBy (fun coin -> coin.Amount) + |> Array.filter (fun coin -> coin.HasSpendKey) + + // 1-based because Array.scan result also includes initial state + let minCoinsIndex = + coins + |> Array.scan (fun acc coin -> acc + coin.Amount) 0L + |> Array.tryFindIndex (fun partialSum -> partialSum >= totalAmount) + + match minCoinsIndex with + | None -> None + | Some coinsIndex -> + let inputs = coins |> Array.take coinsIndex + let inputSum = inputs |> Array.sumBy (fun coin -> coin.Amount) + let maybeRecipient = + if inputSum = totalAmount then + None + else + { + Amount = inputSum - totalAmount + Address = keyChain.GetStealthAddress Coin.ChangeIndex + } + |> Some + Some(inputs, maybeRecipient) + + member private self.Update (transactionOutputs: array) (spentOutputs: array) : Wallet = + let walletWithCoinsSpent = + spentOutputs + |> Array.fold + (fun (wallet: Wallet) outputId -> wallet.MarkAsSpent outputId) + self + + transactionOutputs + |> Array.fold + (fun (wallet: Wallet) output -> (wallet.RewindOutput output) |> fst) + walletWithCoinsSpent + + /// Create MW pegin transaction. Litecoin transaction must have (amount + fee) as its output. + member self.CreatePegInTransaction (amount: Amount) (fee: Amount) : Wallet * Transaction = + let recipient = { Amount = amount; Address = keyChain.GetStealthAddress Coin.PeginIndex } + + let result = + TransactionBuilder.BuildTransaction + Array.empty + (Array.singleton recipient) + Array.empty + (Some(amount + fee)) + fee + + let updatedWallet = + result.Transaction.Body.Outputs + |> Array.fold + (fun (wallet: Wallet) output -> (wallet.RewindOutput output) |> fst) + self + + updatedWallet, result.Transaction + + /// Try to create MW to MW transaction using funds in wallet. If there are insufficient funds, return None. + member self.TryCreateTransaction + (amount: Amount) + (fee: Amount) + (address: StealthAddress) + : Option = + let amountWithFee = amount + fee + + match self.GetInputCoinsAndChangeRecipient amountWithFee with + | None -> None + | Some (inputCoins, maybeChangeRecipient) -> + let recipient = { Amount = amount; Address = address } + let recipients = + match maybeChangeRecipient with + | None -> Array.singleton recipient + | Some changeRecipient -> [| recipient; changeRecipient |] + + let result = + TransactionBuilder.BuildTransaction + inputCoins + recipients + Array.empty + None + fee + + let updatedWallet = + self.Update + result.Transaction.Body.Outputs + (inputCoins |> Array.map (fun coin -> coin.OutputId)) + + Some(updatedWallet, result.Transaction) + + /// Try to create pegout transaction using funds in wallet. If there are insufficient funds, return None. + member self.TryCreatePegOutTransaction + (amount: Amount) + (fee: Amount) + (scriptPubKey: NBitcoin.Script) + : Option = + let amountWithFee = amount + fee + + match self.GetInputCoinsAndChangeRecipient amountWithFee with + | None -> None + | Some (inputCoins, maybeChangeRecipient) -> + let pegoutCoin = { Amount = amount; ScriptPubKey = scriptPubKey } + let recipients = + match maybeChangeRecipient with + | None -> Array.empty + | Some changeRecipient -> Array.singleton changeRecipient + + let result = + TransactionBuilder.BuildTransaction + inputCoins + recipients + (Array.singleton pegoutCoin) + None + fee + + let updatedWallet = + self.Update + result.Transaction.Body.Outputs + (inputCoins |> Array.map (fun coin -> coin.OutputId)) + + Some(updatedWallet, result.Transaction) diff --git a/src/NLitecoin/NLitecoin.fsproj b/src/NLitecoin/NLitecoin.fsproj index 0793909..e83fbd3 100644 --- a/src/NLitecoin/NLitecoin.fsproj +++ b/src/NLitecoin/NLitecoin.fsproj @@ -1,17 +1,23 @@  - netstandard2.0 + netstandard2.1 true 0.1.0.0 0.1.0.0 - 0.1.0 + 0.2.0 ReadMe.md https://github.com/nblockchain/NLitecoin git + + + + + + @@ -23,7 +29,10 @@ - + + + + diff --git a/tests/NLitecoin.MimbleWimble.Tests/NLitecoin.MimbleWimble.Tests.fsproj b/tests/NLitecoin.MimbleWimble.Tests/NLitecoin.MimbleWimble.Tests.fsproj new file mode 100644 index 0000000..5e2f4c5 --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/NLitecoin.MimbleWimble.Tests.fsproj @@ -0,0 +1,51 @@ + + + + net6.0 + + false + false + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/NLitecoin.MimbleWimble.Tests/Program.fs b/tests/NLitecoin.MimbleWimble.Tests/Program.fs new file mode 100644 index 0000000..176a7b6 --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/Program.fs @@ -0,0 +1,4 @@ +module Program = + + [] + let main _ = 0 diff --git a/tests/NLitecoin.MimbleWimble.Tests/SerializationTests.fs b/tests/NLitecoin.MimbleWimble.Tests/SerializationTests.fs new file mode 100644 index 0000000..9215738 --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/SerializationTests.fs @@ -0,0 +1,262 @@ +module NLitecoin.MimbleWimble.SerializationTests + +open System.IO + +open NUnit.Framework +open FsCheck +open FsCheck.NUnit +open NBitcoin + +open NLitecoin.MimbleWimble + + +let roundtripObject<'T when 'T :> ISerializeable and 'T : equality> (object: 'T) (readFunc: BitcoinStream -> 'T) = + use memoryStream = new MemoryStream() + let writeStream = new BitcoinStream(memoryStream, true) + object.Write writeStream + memoryStream.Flush() + memoryStream.Position <- 0 + let readStream = new BitcoinStream(memoryStream, false) + let deserialized = readFunc readStream + deserialized = object + + +type Generators = + static member BigintGenerator (numBytes: int) = + gen { + let! bytes = Gen.listOfLength numBytes (Gen.choose(0, 255) |> Gen.map byte) + return bytes |> List.toArray |> BigInt + } + + static member uint256() = + { new Arbitrary() with + override _.Generator = + Gen.listOfLength 32 (Gen.choose(0, 255) |> Gen.map byte) + |> Gen.map (List.toArray >> uint256) } + + static member PedersenCommitment() = + { new Arbitrary() with + override _.Generator = + Generators.BigintGenerator PedersenCommitment.NumBytes + |> Gen.map PedersenCommitment } + + static member Signature() = + { new Arbitrary() with + override _.Generator = + Generators.BigintGenerator Signature.NumBytes + |> Gen.map Signature } + + static member PublicKey() = + { new Arbitrary() with + override _.Generator = + Generators.BigintGenerator PublicKey.NumBytes + |> Gen.map PublicKey } + + static member Input() = + { new Arbitrary() with + override _.Generator = + gen { + let! features = + Gen.elements + [ + InputFeatures.EXTRA_DATA_FEATURE_BIT + InputFeatures.STEALTH_KEY_FEATURE_BIT + InputFeatures.EXTRA_DATA_FEATURE_BIT ||| InputFeatures.STEALTH_KEY_FEATURE_BIT + ] + let! outputId = Arb.generate + let! commitment = Arb.generate + let! outputPubKey = Arb.generate + let! signature = Arb.generate + let! inputPubKeyValue = Arb.generate + let inputPubKey = + if int(features &&& InputFeatures.STEALTH_KEY_FEATURE_BIT) <> 0 then + Some inputPubKeyValue + else + None + let! extraData = + let len = + if int(features &&& InputFeatures.EXTRA_DATA_FEATURE_BIT) <> 0 then + 100 + else + 0 + Gen.listOfLength len Arb.generate + |> Gen.map List.toArray + return { + Features = features + OutputID = outputId + Commitment = commitment + InputPublicKey = inputPubKey + OutputPublicKey = outputPubKey + ExtraData = extraData + Signature = signature + } + } } + + static member OutputMessage() = + { new Arbitrary() with + override _.Generator = + gen { + let! features = + Gen.elements + [ + OutputFeatures.EXTRA_DATA_FEATURE_BIT + OutputFeatures.STANDARD_FIELDS_FEATURE_BIT + OutputFeatures.EXTRA_DATA_FEATURE_BIT ||| OutputFeatures.STANDARD_FIELDS_FEATURE_BIT + ] + let! standardFieldsValue = Arb.generate + let! maskedNonce = Generators.BigintGenerator OutputMessageStandardFields.MaskedNonceNumBytes + let standardFields = + if int(features &&& OutputFeatures.STANDARD_FIELDS_FEATURE_BIT) <> 0 then + Some { standardFieldsValue with MaskedNonce = maskedNonce } + else + None + let! extraData = + let len = + if int(features &&& OutputFeatures.EXTRA_DATA_FEATURE_BIT) <> 0 then + 100 + else + 0 + Gen.listOfLength len Arb.generate + |> Gen.map List.toArray + return { + Features = features + StandardFields = standardFields + ExtraData = extraData + } + } } + + static member RangeProof() = + { new Arbitrary() with + override _.Generator = + gen { + let! data = Gen.listOfLength RangeProof.Size Arb.generate + return RangeProof(data |> List.toArray) + } } + + static member PegOutCoin() = + { new Arbitrary() with + override _.Generator = + gen { + let! camount = Arb.generate + return { + Amount = camount + ScriptPubKey = NBitcoin.Script.Empty + } + } } + + static member Kernel() = + { new Arbitrary() with + override _.Generator = + gen { + let! fetauresList = Gen.subListOf (KernelFeatures.GetValues()) + let features = fetauresList |> List.fold (fun a b -> a ||| b) (enum 0) + let! fee = + if int(features &&& KernelFeatures.FEE_FEATURE_BIT) <> 0 then + Arb.generate |> Gen.map Some + else + Gen.constant None + let! pegin = + if int(features &&& KernelFeatures.PEGIN_FEATURE_BIT) <> 0 then + Arb.generate |> Gen.map Some + else + Gen.constant None + let! pegouts = + if int(features &&& KernelFeatures.PEGOUT_FEATURE_BIT) <> 0 then + Arb.generate |> Gen.nonEmptyListOf + else + Gen.constant List.empty + let! lockHeight = + if int(features &&& KernelFeatures.HEIGHT_LOCK_FEATURE_BIT) <> 0 then + Arb.generate |> Gen.map Some + else + Gen.constant None + let! stealthExcess = + if int(features &&& KernelFeatures.STEALTH_EXCESS_FEATURE_BIT) <> 0 then + Arb.generate |> Gen.map Some + else + Gen.constant None + let! extraData = + if int(features &&& KernelFeatures.EXTRA_DATA_FEATURE_BIT) <> 0 then + Arb.generate |> Gen.nonEmptyListOf + else + Gen.constant List.empty + let! excess = Arb.generate + let! signature = Arb.generate + return { + Features = features + Fee = fee + Pegin = pegin + Pegouts = pegouts |> List.toArray + LockHeight = lockHeight + StealthExcess = stealthExcess + ExtraData = extraData |> List.toArray + Excess = excess + Signature = signature + } + } } + +[|])>] +let Uint256Roundtrip(number: uint256) = + use memoryStream = new MemoryStream() + let writeStream = new BitcoinStream(memoryStream, true) + Helpers.WriteUint256 writeStream number + memoryStream.Flush() + memoryStream.Position <- 0 + let readStream = new BitcoinStream(memoryStream, false) + Helpers.ReadUint256 readStream = number + +[|])>] +let PedersenCommitmentRoundtrip(commitment: PedersenCommitment) = + roundtripObject commitment PedersenCommitment.Read + +[|])>] +let PublicKeyRoundtrip(pubKey: PublicKey) = + roundtripObject pubKey PublicKey.Read + +[|])>] +let SignatureRoundtrip(signature: Signature) = + roundtripObject signature Signature.Read + +[|])>] +let InputRoundtrip(input: Input) = + roundtripObject input Input.Read + +[|])>] +let OutputMessageRoundtrip(input: OutputMessage) = + roundtripObject input OutputMessage.Read + +[|])>] +let OutputRoundtrip(output: Output) = + roundtripObject output Output.Read + +[|])>] +let PegOutCoinRoundtrip(pegoutCoin: PegOutCoin) = + roundtripObject pegoutCoin PegOutCoin.Read + +[|])>] +let KernelRoundtrip(kernel: Kernel) = + roundtripObject kernel Kernel.Read + +[|])>] +let TxBodyRoundtrip(txBody: TxBody) = + roundtripObject txBody TxBody.Read + +[|])>] +let TransactionRoundtrip(transaction: Transaction) = + roundtripObject transaction Transaction.Read + +/// Deserialize transaction generated and serialized by modified litecoin test +/// (see https://github.com/litecoin-project/litecoin/blob/5ac781487cc9589131437b23c69829f04002b97e/src/libmw/test/tests/models/tx/Test_Transaction.cpp) +[] +let TestTransactionDeserilaization() = + let serializedTransaction = File.ReadAllBytes "transaction.bin" + let bitcoinStream = BitcoinStream serializedTransaction + let transaction = Transaction.Read bitcoinStream + + Assert.AreEqual(transaction.Body.Kernels[0].Pegin, Some 123L) + Assert.AreEqual(transaction.Body.Kernels[1].Fee, Some 5L) + +[|])>] +let StealthAddressStringEncodingRoundtrip(stealthAddress: StealthAddress) = + let encoded = stealthAddress.EncodeDestination() + StealthAddress.DecodeDestination encoded = stealthAddress diff --git a/tests/NLitecoin.MimbleWimble.Tests/TransactionBuilderTests.fs b/tests/NLitecoin.MimbleWimble.Tests/TransactionBuilderTests.fs new file mode 100644 index 0000000..206a7e8 --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/TransactionBuilderTests.fs @@ -0,0 +1,145 @@ +module NLitecoin.MimbleWimble.TransactionBuilderTests + +open NUnit.Framework + +open NLitecoin.MimbleWimble +open NLitecoin.MimbleWimble.TransactionBuilder + +let GetRandomPubKey() = + let bytes = NBitcoin.RandomUtils.GetBytes 32 + NBitcoin.Secp256k1.ECPrivKey.Create(bytes).CreatePubKey().ToBytes(true) + |> BigInt + |> PublicKey + +let GetRandomStealthAddress() : StealthAddress = + { + ScanPubKey = GetRandomPubKey() + SpendPubKey = GetRandomPubKey() + } + +[] +let PegInTransactionTest() = + let amount = 1000L + let recipient = { Amount = amount; Address = GetRandomStealthAddress() } + let fee = 100L + + let result = + TransactionBuilder.BuildTransaction + Array.empty + (Array.singleton recipient) + Array.empty + (Some(amount + fee)) + fee + + match result.OutputCoins with + | [| outputCoin |] -> + Assert.AreEqual(amount, outputCoin.Amount) + Assert.AreEqual(Some recipient.Address, outputCoin.Address) + | _ -> Assert.Fail "Exactly 1 output coin expected" + + Validation.ValidateTransactionBody result.Transaction.Body + Validation.ValidateKernelSumForTransaction result.Transaction + +[] +let PegOutTransactionTest() = + let amount = 1000L + let fee = 100L + let pegoutCoin = + { Amount = amount; ScriptPubKey = NBitcoin.Script.Empty } + let inputCoin = + { Coin.Empty with + Blind = Some <| BlindingFactor (NBitcoin.RandomUtils.GetUInt256()) + SpendKey = Some (NBitcoin.RandomUtils.GetUInt256()) + Amount = amount + fee } + + let result = + BuildTransaction + (Array.singleton inputCoin) + Array.empty + (Array.singleton pegoutCoin) + None + fee + + Assert.IsEmpty(result.OutputCoins) + + Validation.ValidateTransactionBody result.Transaction.Body + Validation.ValidateKernelSumForTransaction result.Transaction + +[] +let MWTransactionTest() = + let amount = 1000L + let fee = 100L + let inputCoin = + { Coin.Empty with + Blind = Some <| BlindingFactor (NBitcoin.RandomUtils.GetUInt256()) + SpendKey = Some (NBitcoin.RandomUtils.GetUInt256()) + Amount = amount + fee } + let recipient = { Amount = amount; Address = GetRandomStealthAddress() } + + let result = + BuildTransaction + (Array.singleton inputCoin) + (Array.singleton recipient) + Array.empty + None + fee + + match result.OutputCoins with + | [| outputCoin |] -> + Assert.AreEqual(amount, outputCoin.Amount) + Assert.AreEqual(Some recipient.Address, outputCoin.Address) + | _ -> Assert.Fail "Exactly 1 output coin expected" + + Validation.ValidateTransactionBody result.Transaction.Body + Validation.ValidateKernelSumForTransaction result.Transaction + +[] +let MWTransactionTest2() = + // send part of the funds + let balance = 3000L + let amount = 1000L + let fee = 100L + let inputCoin = + { Coin.Empty with + Blind = Some <| BlindingFactor (NBitcoin.RandomUtils.GetUInt256()) + SpendKey = Some (NBitcoin.RandomUtils.GetUInt256()) + Address = Some(GetRandomStealthAddress()) + Amount = balance } + let recipient = { Amount = amount; Address = GetRandomStealthAddress() } + let recipientUnspent = { Amount = balance - amount - fee; Address = GetRandomStealthAddress() } + + let result = + BuildTransaction + (Array.singleton inputCoin) + [| recipient; recipientUnspent |] + Array.empty + None + fee + + match result.OutputCoins with + | [| outputCoin; outputCoinUnspent |] -> + Assert.AreEqual(amount, outputCoin.Amount) + Assert.AreEqual(Some recipient.Address, outputCoin.Address) + Assert.AreEqual(recipientUnspent.Amount, outputCoinUnspent.Amount) + Assert.AreEqual(Some recipientUnspent.Address, outputCoinUnspent.Address) + | _ -> Assert.Fail "Exactly 2 output coins expected" + + Validation.ValidateTransactionBody result.Transaction.Body + Validation.ValidateKernelSumForTransaction result.Transaction + +[] +let InvalidTransactionsTest() = + let amount = 1000L + let recipient = { Amount = amount; Address = GetRandomStealthAddress() } + let fee = 100L + + Assert.Throws + (fun _ -> + BuildTransaction + Array.empty + (Array.singleton recipient) + Array.empty + (Some amount) // fee should be added here, but it's not + fee + |> ignore) + |> ignore diff --git a/tests/NLitecoin.MimbleWimble.Tests/TransactionTests.fs b/tests/NLitecoin.MimbleWimble.Tests/TransactionTests.fs new file mode 100644 index 0000000..596cdec --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/TransactionTests.fs @@ -0,0 +1,81 @@ +module NLitecoin.MimbleWimble.TransactionTests + +open System + +open NUnit.Framework + +open NLitecoin.MimbleWimble + +[] +let Setup () = + () + +[] +let ParsePegInTransaction () = + let rawTransaction = IO.File.ReadAllText "transaction1.txt" + + let litecoinTransaction = + NBitcoin.Transaction.Parse(rawTransaction, NLitecoin.Litecoin.Instance.Mainnet) + + let transaction = + (litecoinTransaction :?> NLitecoin.LitecoinTransaction).MimbleWimbleTransaction.Value + + Assert.AreEqual(0, transaction.Body.Inputs.Length) + Assert.AreEqual(1, transaction.Body.Kernels.Length) + Assert.GreaterOrEqual(transaction.Body.Outputs.Length, 1) + + Assert.IsTrue(transaction.Body.Kernels.[0].Pegin.IsSome) + Assert.IsEmpty(transaction.Body.Kernels.[0].Pegouts) + + Validation.ValidateTransactionBody transaction.Body + Validation.ValidateKernelSumForTransaction transaction + +[] +let ParseMWTransaction () = + let rawTransaction = IO.File.ReadAllText "transaction2.txt" + + let litecoinTransaction = + NBitcoin.Transaction.Parse(rawTransaction, NLitecoin.Litecoin.Instance.Mainnet) + + let transaction = + (litecoinTransaction :?> NLitecoin.LitecoinTransaction).MimbleWimbleTransaction.Value + + Assert.GreaterOrEqual(transaction.Body.Inputs.Length, 1) + Assert.AreEqual(1, transaction.Body.Kernels.Length) + Assert.GreaterOrEqual(transaction.Body.Outputs.Length, 1) + + Assert.IsTrue(transaction.Body.Kernels.[0].Pegin.IsNone) + Assert.IsEmpty(transaction.Body.Kernels.[0].Pegouts) + + Validation.ValidateTransactionBody transaction.Body + Validation.ValidateKernelSumForTransaction transaction + +[] +let ParsePegOutTransaction () = + let rawTransaction = IO.File.ReadAllText "transaction3.txt" + + let litecoinTransaction = + NBitcoin.Transaction.Parse(rawTransaction, NLitecoin.Litecoin.Instance.Mainnet) + + let transaction = + (litecoinTransaction :?> NLitecoin.LitecoinTransaction).MimbleWimbleTransaction.Value + + Assert.GreaterOrEqual(transaction.Body.Inputs.Length, 1) + Assert.AreEqual(1, transaction.Body.Kernels.Length) + Assert.GreaterOrEqual(transaction.Body.Outputs.Length, 1) + + Assert.IsTrue(transaction.Body.Kernels.[0].Pegin.IsNone) + Assert.AreEqual(97490L, transaction.Body.Kernels.[0].Pegouts.[0].Amount) + + Validation.ValidateTransactionBody transaction.Body + Validation.ValidateKernelSumForTransaction transaction + +[] +let ParseBlockWithHogExTransaction () = + // Check if HogEx transaction, which has mweb extension flag but doesn't contain MW transaction, is pardsed correctly + let blockData = IO.File.ReadAllText "block1.txt" + let block = NBitcoin.Block.Parse(blockData, NLitecoin.Litecoin.Instance.Mainnet) + // HogEx transaction must be at the end of the block + // (see https://github.com/litecoin-project/lips/blob/master/lip-0002.mediawiki#user-content-Integrating_Transaction_ExtTxn) + let lastTransaction = block.Transactions.[block.Transactions.Count - 1] :?> NLitecoin.LitecoinTransaction + Assert.IsTrue(lastTransaction.MimbleWimbleTransaction.IsNone) diff --git a/tests/NLitecoin.MimbleWimble.Tests/Validation.fs b/tests/NLitecoin.MimbleWimble.Tests/Validation.fs new file mode 100644 index 0000000..26b9457 --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/Validation.fs @@ -0,0 +1,62 @@ +module NLitecoin.MimbleWimble.Validation + +open NUnit.Framework + +open NLitecoin.MimbleWimble + +let ValidateTransactionBody (txBody: TxBody) = + CollectionAssert.IsOrdered txBody.Inputs + CollectionAssert.IsOrdered txBody.Outputs + CollectionAssert.IsOrdered txBody.Kernels + + let spentIds = txBody.Inputs |> Array.map (fun input -> input.OutputID) + CollectionAssert.AllItemsAreUnique spentIds + + let outputIds = txBody.Outputs |> Array.map Hasher.CalculateHash + CollectionAssert.AllItemsAreUnique outputIds + + let kernelIds = txBody.Kernels |> Array.map Hasher.CalculateHash + CollectionAssert.AllItemsAreUnique kernelIds + + use bulletproof = new Secp256k1ZKP.Net.BulletProof() + for output in txBody.Outputs do + let commitmentBytes = + match output.Commitment with + | PedersenCommitment bigint -> bigint.Data + let rangeProofBytes = + match output.RangeProof with + | RangeProof bytes -> bytes + let messsageSerialized = + use memoryStream = new System.IO.MemoryStream() + let stream = new NBitcoin.BitcoinStream(memoryStream, true) + (output.Message :> ISerializeable).Write stream + memoryStream.ToArray() + + bulletproof.Verify(commitmentBytes, rangeProofBytes, messsageSerialized) + |> Assert.IsTrue + +let ValidateKernelSumForTransaction (transaction: Transaction) = + let inputCommits = transaction.Body.Inputs |> Array.map (fun input -> input.Commitment) + let outputCommits = transaction.Body.Outputs |> Array.map (fun output -> output.Commitment) + let kernelCommits = transaction.Body.Kernels |> Array.map (fun kernel -> kernel.Excess) + let coinsAdded = transaction.Body.Kernels |> Array.sumBy (fun kernel -> kernel.GetSupplyChange()) + + let sumUtxoCommitment = + if coinsAdded > 0L then + Pedersen.AddCommitments + outputCommits + (Array.append inputCommits [| Pedersen.Commit coinsAdded (BlindingFactor NBitcoin.uint256.Zero) |]) + elif coinsAdded < 0L then + Pedersen.AddCommitments + (Array.append outputCommits [| Pedersen.Commit (abs coinsAdded) (BlindingFactor NBitcoin.uint256.Zero) |]) + inputCommits + else + Pedersen.AddCommitments outputCommits inputCommits + + let sumExcessCommitment = + if transaction.KernelOffset.ToUInt256() <> NBitcoin.uint256.Zero then + Pedersen.AddCommitments (Array.append kernelCommits [| Pedersen.Commit 0 transaction.KernelOffset |]) Array.empty + else + Pedersen.AddCommitments kernelCommits Array.empty + + Assert.AreEqual(sumUtxoCommitment, sumExcessCommitment) diff --git a/tests/NLitecoin.MimbleWimble.Tests/WalletTests.fs b/tests/NLitecoin.MimbleWimble.Tests/WalletTests.fs new file mode 100644 index 0000000..eb27c79 --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/WalletTests.fs @@ -0,0 +1,81 @@ +module NLitecoin.MimbleWimble.WalletTests + +open NUnit.Framework + +open NBitcoin + +open NLitecoin.MimbleWimble +open NLitecoin.MimbleWimble.Wallet + +// see https://github.com/litecoin-project/litecoin/blob/5ac781487cc9589131437b23c69829f04002b97e/src/wallet/test/scriptpubkeyman_tests.cpp#L44 +let walletSeed = + let key = + let data = + DataEncoders.Base58CheckEncoder().DecodeData("6usgJoGKXW12i7Ruxy8Z1C5hrRMVGfLmi9NU9uDQJMPXDJ6tQAH") + new Key(data.[1..32], 32, false) + key.ToBytes() + +[] +let TestKeyDerivation() = + let keyChain = KeyChain walletSeed + + Assert.AreEqual( + "2396e5c33b07dfa2d9e70da1dcbdad0ad2399e5672ff2d4afbe3b20bccf3ba1b", + keyChain.SpendKey.PrivateKey.ToHex()) + Assert.AreEqual( + "918271168655385e387907612ee09d755be50c4685528f9f53eabae380ecba97", + keyChain.ScanKey.PrivateKey.ToHex()) + +[] +let TestStealthAddressGeneration() = + let keyChain = KeyChain walletSeed + + let changeAddress = keyChain.GetStealthAddress 0u + Assert.AreEqual( + "ltcmweb1qq20e2arnhvxw97katjkmsd35agw3capxjkrkh7dk8d30rczm8ypxuq329nwh2twmchhqn3jqh7ua4ps539f6aazh79jy76urqht4qa59ts3at6gf", + changeAddress.EncodeDestination()) + + let peginAddress = keyChain.GetStealthAddress 1u + Assert.AreEqual( + "ltcmweb1qqg5hddkl4uhspjwg9tkmatxa4s6gswdaq9swl8vsg5xxznmye7phcqatzc62mzkg788tsrfcuegxe9q3agf5cplw7ztqdusqf7x3n2tl55x4gvyt", + peginAddress.EncodeDestination()) + + let receiveAddress = keyChain.GetStealthAddress 2u + Assert.AreEqual( + "ltcmweb1qq0yq03ewm830ugmkkvrvjmyyeslcpwk8ayd7k27qx63sryy6kx3ksqm3k6jd24ld3r5dp5lzx7rm7uyxfujf8sn7v4nlxeqwrcq6k6xxwqdc6tl3", + receiveAddress.EncodeDestination()) + +[] +let TestWallet() = + let keyChain = KeyChain walletSeed + let wallet = Wallet keyChain + + let initialAmount = 10000L + let fee = 100L + + let walletAfterPegin, peginTx = wallet.CreatePegInTransaction initialAmount fee + Assert.AreEqual(initialAmount, walletAfterPegin.GetBalance()) + + let payment1Amount = 2000L + let payment1Address = TransactionBuilderTests.GetRandomStealthAddress() + let walletAfterPayment1, payment1Tx = + (walletAfterPegin.TryCreateTransaction payment1Amount fee payment1Address).Value + Assert.AreEqual(initialAmount - fee - payment1Amount, walletAfterPayment1.GetBalance()) + + // not enough funds + Assert.AreEqual(None, walletAfterPayment1.TryCreateTransaction initialAmount fee payment1Address) + Assert.AreEqual(None, walletAfterPayment1.TryCreatePegOutTransaction initialAmount fee Script.Empty) + + let pegOutAmount = 3000L + let walletAfterPegOut, pegOutTx = + (walletAfterPayment1.TryCreatePegOutTransaction pegOutAmount fee Script.Empty).Value + Assert.AreEqual(initialAmount - fee - payment1Amount - fee - pegOutAmount, walletAfterPegOut.GetBalance()) + + let walletRestoredFromTransactions = + [ peginTx; payment1Tx; pegOutTx ] + |> List.fold + (fun (currWallet: Wallet) tx -> currWallet.ProcessTransaction tx) + wallet + + Assert.AreEqual(walletAfterPegOut.GetBalance(), walletRestoredFromTransactions.GetBalance()) + CollectionAssert.AreEquivalent(walletAfterPegOut.GetUnspentCoins(), walletRestoredFromTransactions.GetUnspentCoins()) diff --git a/tests/NLitecoin.MimbleWimble.Tests/ZKPTests.fs b/tests/NLitecoin.MimbleWimble.Tests/ZKPTests.fs new file mode 100644 index 0000000..6894b84 --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/ZKPTests.fs @@ -0,0 +1,309 @@ +module NLitecoin.MimbleWimble.ZKPTests + +// Differential tests for zero-knowledge proof components that use https://github.com/tangramproject/Secp256k1-ZKP.Net as reference + +open System +open System.Runtime.InteropServices + +open NUnit.Framework +open FsCheck +open FsCheck.NUnit +open Org.BouncyCastle.Math +open NBitcoin + +open NLitecoin.MimbleWimble +open NLitecoin.MimbleWimble.EC + +type ByteArray32Generators = + static member ByteArray() = + { new Arbitrary>() with + override _.Generator = + Gen.arrayOfLength 32 (Gen.choose(0, 255) |> Gen.map byte) } + + static member UInt256() = + { new Arbitrary() with + override _.Generator = + Arb.generate> |> Gen.map uint256 } + + static member BlindingFactor() = + { new Arbitrary() with + override _.Generator = + gen { + let! bytes = Arb.generate> + let! leadingZeros = Gen.elements [ 0; 0; 0; 1; 31 ] + Array.fill bytes 0 leadingZeros 0uy + return bytes |> NBitcoin.uint256 |> BlindingFactor } } + +type private Secp256k1ZKpBulletproof() = + inherit Secp256k1ZKP.Net.BulletProof() + + [] + static extern uint64 secp256k1_bulletproof_innerproduct_proof_length(uint64 n) + + [] + static extern int secp256k1_generator_generate(nativeint ctx, IntPtr gen, byte[] key32) + + [] + static extern int secp256k1_generator_serialize(nativeint ctx, byte[] output, IntPtr gen) + + member self.InnerproductProofLength(n: uint64) : uint64 = + secp256k1_bulletproof_innerproduct_proof_length(n) + + member self.GeneratorGenerate(key: array) = + let gen = Marshal.AllocHGlobal 64 + let opResult = secp256k1_generator_generate(self.Context, gen, key) + assert(opResult <> 0) + assert(gen <> IntPtr.Zero) + let output = Array.zeroCreate 33 + let opResult2 = secp256k1_generator_serialize(self.Context, output, gen) + assert(opResult2 <> 0) + Marshal.FreeHGlobal gen + output + +[] +let TestSchnorrSign () = + let test message key expected = + let signature = SchnorrSign key message + let signatureBytes = + match signature with + | Signature bigint -> bigint.Data + Assert.AreEqual(expected, signatureBytes) + + // Test vectors (see https://github.com/litecoin-project/litecoin/blob/5ac781487cc9589131437b23c69829f04002b97e/src/secp256k1-zkp/src/modules/schnorrsig/tests_impl.h#L168) + let key1 = Array.zeroCreate 32 |> Array.updateAt 31 1uy + let msg1 = Array.zeroCreate 32 + let expected1 = + "787A848E71043D280C50470E8E1532B2DD5D20EE912A45DBDD2BD1DFBF187EF67031A98831859DC34DFFEEDDA86831842CCD0079E1F92AF177F7F22CC1DCED05" + |> Convert.FromHexString + + test msg1 key1 expected1 + + let key2 = "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF" |> Convert.FromHexString + let msg2 = "243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89" |> Convert.FromHexString + let expected2 = + "2A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D1E51A22CCEC35599B8F266912281F8365FFC2D035A230434A1A64DC59F7013FD" + |> Convert.FromHexString + + test msg2 key2 expected2 + + let key3 = "C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C7" |> Convert.FromHexString + let msg3 = "5E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C" |> Convert.FromHexString + let expected3 = + "00DA9B08172A9B6F0466A2DEFD817F2D7AB437E0D253CB5395A963866B3574BE00880371D01766935B92D2AB4CD5C8A2A5837EC57FED7660773A05F0DE142380" + |> Convert.FromHexString + + test msg3 key3 expected3 + +[|])>] +let TestPedersenCommit (value: uint64) (blind: BlindingFactor) = + blind.ToUInt256() <> uint256.Zero ==> fun () -> + use pedersen = new Secp256k1ZKP.Net.Pedersen() + let referenceCommitment = pedersen.Commit(value, blind.ToUInt256().ToBytes()) + let ourCommitment = Pedersen.Commit (int64 value) blind + ourCommitment = PedersenCommitment(BigInt referenceCommitment) + +[|])>] +let TestBlindSwitch (value: uint64) (blind: BlindingFactor) = + blind.ToUInt256() <> uint256.Zero ==> fun () -> + use pedersen = new Secp256k1ZKP.Net.Pedersen() + let referenceBlind = pedersen.BlindSwitch(value, blind.ToUInt256().ToBytes()) + let ourBlind = Pedersen.BlindSwitch blind (int64 value) + ourBlind = BlindingFactor(uint256 referenceBlind) + +[|])>] +let TestBlindingFactor (factor: BlindingFactor) = + let bytes = factor.ToUInt256().ToBytes() + (bytes |> BigInteger.FromByteArrayUnsigned |> EC.curve.Curve.FromBigInteger).GetEncoded() = bytes + +[|])>] +let TestBigIntegerToUInt256 (bytes: array) = + let integer = bytes |> BigInteger.FromByteArrayUnsigned + integer = (integer.ToUInt256().ToBytes() |> Org.BouncyCastle.Math.BigInteger.FromByteArrayUnsigned) + +[|])>] +let TestByteArrayToUInt256 (bytes: array) = + bytes = (bytes |> uint256).ToBytes() + +[|])>] +let TestAddBlindingFactors (positive: array) (negative: array) = + use pedersen = new Secp256k1ZKP.Net.Pedersen() + let referenceSum = + pedersen.BlindSum( + positive |> Array.map (fun each -> each.ToUInt256().ToBytes()), + negative |> Array.map (fun each -> each.ToUInt256().ToBytes()) + ) + let ourSum = Pedersen.AddBlindingFactors positive negative + ourSum.ToUInt256().ToBytes() = referenceSum + +[|], MaxTest=20)>] +let TestRangeProofCanBeVerified + (amount: uint64) + (key: uint256) + (privateNonce: uint256) + (rewindNonce: uint256) + (extraData: Option>) = + + let commit = + match Pedersen.Commit (int64 amount) (BlindingFactor <| key) with + | PedersenCommitment num -> num.Data + + let proofMessage = Array.zeroCreate 20 + let proof = Bulletproof.ConstructRangeProof amount key privateNonce rewindNonce proofMessage extraData + let proofData = + match proof with + | RangeProof data -> data + + let extraDataAsNullable = + match extraData with + | Some array -> array + | None -> null + + use secp256k1ZKPBulletProof = new Secp256k1ZKpBulletproof() + // argument names are wrong here: 3rd param should be rewindNonce and 4th nonce + //let proofZKP = secp256k1ZKPBulletProof.ProofSingle(amount, key.ToBytes(), rewindNonce.ToBytes(), privateNonce.ToBytes(), extraDataAsNullable, proofMessage) + + secp256k1ZKPBulletProof.Verify(commit, proofData, extraDataAsNullable) + +[] +let TestInnerproductProofLength() = + use secp256k1ZKPBulletProof = new Secp256k1ZKpBulletproof() + try + for exp=0 to 16 do + let n = pown 2 (int exp) + let ourLength = Bulletproof.InnerProductProofLength(int n) + let referenceLength = secp256k1ZKPBulletProof.InnerproductProofLength(uint64 n) + Assert.AreEqual(referenceLength, uint64 ourLength) + with + | :? System.EntryPointNotFoundException -> + Assert.Inconclusive "no secp256k1_bulletproof_innerproduct_proof_length in libsecp256k1 on Linux" + +[|])>] +let TestGeneratorGenerate(key: array) = + use secp256k1ZKPBulletProof = new Secp256k1ZKpBulletproof() + let referenceGenerator = secp256k1ZKPBulletProof.GeneratorGenerate key + + let ourGenerator = Bulletproof.GeneratorGenerate key + let ourGeneratorSerialized = ourGenerator.GetEncoded true + + // skip first byte since serialization formats are different + referenceGenerator.[1..] = ourGeneratorSerialized.[1..] + +[] +let TestRfc6979HmacSha256() = + // output from modified secp256k1-zkp tests + // first 2 keys generated in secp256k1_bulletproof_generators_create + // from generatorG as seed + let referenceKeys = + [| + "edc883a98f9ad8dad390a2c814647b6dac92aed530da554db914ea4f8ad988c7" + "d99994e5535e0788752493113103145529e20b38e1c68dc28f67816b2a85b65f" + |] + |> Array.map(fun str -> Convert.FromHexString str) + + let ourKeys = + let seed = Array.append (generatorG.XCoord.GetEncoded()) (generatorG.YCoord.GetEncoded()) + let rng = Bulletproof.Rfc6979HmacSha256 seed + Array.init 2 (fun _ -> rng.Generate 32) + + Array.iter2 + (fun refKey ourKey -> Assert.AreEqual(refKey, ourKey)) + referenceKeys + ourKeys + +[] +let TestScalarChaCha20() = + // see https://github.com/litecoin-project/litecoin/blob/5ac781487cc9589131437b23c69829f04002b97e/src/secp256k1-zkp/src/tests.c#L1010 + let seed1 = uint256(Array.zeroCreate 32) + + let expected1l = + [| + 0x76uy; 0xb8uy; 0xe0uy; 0xaduy; 0xa0uy; 0xf1uy; 0x3duy; 0x90uy + 0x40uy; 0x5duy; 0x6auy; 0xe5uy; 0x53uy; 0x86uy; 0xbduy; 0x28uy + 0xbduy; 0xd2uy; 0x19uy; 0xb8uy; 0xa0uy; 0x8duy; 0xeduy; 0x1auy + 0xa8uy; 0x36uy; 0xefuy; 0xccuy; 0x8buy; 0x77uy; 0x0duy; 0xc7uy + |] + |> BigInteger.FromByteArrayUnsigned + let expected1r = + [| + 0xdauy; 0x41uy; 0x59uy; 0x7cuy; 0x51uy; 0x57uy; 0x48uy; 0x8duy + 0x77uy; 0x24uy; 0xe0uy; 0x3fuy; 0xb8uy; 0xd8uy; 0x4auy; 0x37uy + 0x6auy; 0x43uy; 0xb8uy; 0xf4uy; 0x15uy; 0x18uy; 0xa1uy; 0x1cuy + 0xc3uy; 0x87uy; 0xb6uy; 0x69uy; 0xb2uy; 0xeeuy; 0x65uy; 0x86uy + |] + |> BigInteger.FromByteArrayUnsigned + + Assert.Less(expected1l, scalarOrder) + Assert.Less(expected1r, scalarOrder) + + let l, r = Bulletproof.ScalarChaCha20 seed1 0UL + + Assert.AreEqual(expected1l, l) + Assert.AreEqual(expected1r, r) + + let expected2l = + [| + 0x45uy; 0x40uy; 0xf0uy; 0x5auy; 0x9fuy; 0x1fuy; 0xb2uy; 0x96uy + 0xd7uy; 0x73uy; 0x6euy; 0x7buy; 0x20uy; 0x8euy; 0x3cuy; 0x96uy + 0xebuy; 0x4fuy; 0xe1uy; 0x83uy; 0x46uy; 0x88uy; 0xd2uy; 0x60uy + 0x4fuy; 0x45uy; 0x09uy; 0x52uy; 0xeduy; 0x43uy; 0x2duy; 0x41uy + |] + |> BigInteger.FromByteArrayUnsigned + let expected2r = + [| + 0xbbuy; 0xe2uy; 0xa0uy; 0xb6uy; 0xeauy; 0x75uy; 0x66uy; 0xd2uy + 0xa5uy; 0xd1uy; 0xe7uy; 0xe2uy; 0x0duy; 0x42uy; 0xafuy; 0x2cuy + 0x53uy; 0xd7uy; 0x92uy; 0xb1uy; 0xc4uy; 0x3fuy; 0xeauy; 0x81uy + 0x7euy; 0x9auy; 0xd2uy; 0x75uy; 0xaeuy; 0x54uy; 0x69uy; 0x63uy + |] + |> BigInteger.FromByteArrayUnsigned + + let seed2 = seed1.ToBytes() |> Array.updateAt 31 1uy |> uint256 + let l2, r2 = Bulletproof.ScalarChaCha20 seed2 0UL + + Assert.AreEqual(expected2l, l2) + Assert.AreEqual(expected2r, r2) + + let expected3l = + [| + 0x47uy; 0x4auy; 0x4fuy; 0x35uy; 0x4fuy; 0xeeuy; 0x93uy; 0x59uy + 0xbbuy; 0x65uy; 0x81uy; 0xe5uy; 0xd9uy; 0x15uy; 0xa6uy; 0x01uy + 0xb6uy; 0x8cuy; 0x68uy; 0x03uy; 0x38uy; 0xffuy; 0x65uy; 0xe6uy + 0x56uy; 0x4auy; 0x3euy; 0x65uy; 0x59uy; 0xfcuy; 0x12uy; 0x3fuy + |] + |> BigInteger.FromByteArrayUnsigned + let expected3r = + [| + 0xa9uy; 0xb2uy; 0xf9uy; 0x3euy; 0x57uy; 0xc3uy; 0xa5uy; 0xcbuy + 0xe0uy; 0x72uy; 0x74uy; 0x27uy; 0x88uy; 0x1cuy; 0x23uy; 0xdfuy + 0xe2uy; 0xb6uy; 0xccuy; 0xfbuy; 0x93uy; 0xeduy; 0xcbuy; 0x02uy + 0xd7uy; 0x50uy; 0x52uy; 0x45uy; 0x84uy; 0x88uy; 0xbbuy; 0xeauy + |] + |> BigInteger.FromByteArrayUnsigned + + let l3, r3 = Bulletproof.ScalarChaCha20 seed2 100UL + + Assert.AreEqual(expected3l, l3) + Assert.AreEqual(expected3r, r3) + +[] +let TestUpdateCommit() = + // output from modified secp256k1-zkp tests + let commit = + "ea47aaa6e111d44f973cffff730dc3f5a41cd1d30687bbfcd8b91ad8fc9d63e6" + |> Convert.FromHexString + |> uint256 + let lpt = + let x = "2dc4b4f3b3d9530c5d1ab2d7fe12291be0aa7c4a0b5ccf6125c958a2867d652a" + let y = "6d5e07c347e778672126cb47a8a26d40e84b0639805b219c129c1f34be51a8ba" + curve.Curve.CreatePoint(BigInteger(x, 16), BigInteger(y, 16)) + let rpt = + let x = "62a7bb4d9ab0ff01363368093354af7941d058ebd16a1cd3bd21cfc6401d5112" + let y = "44f98a209695e93c28e4293dd9cb113affa4d92e8f1dbdb907d8bcb6271617a9" + curve.Curve.CreatePoint(BigInteger(x, 16), BigInteger(y, 16)) + + let expected = + "8370c779a784b2188e14f5bf5f936df110361f0fbefee873c9a75e8d928cf4d9" + |> Convert.FromHexString + |> uint256 + + Assert.AreEqual(expected, Bulletproof.UpdateCommit commit lpt rpt) diff --git a/tests/NLitecoin.MimbleWimble.Tests/block1.txt b/tests/NLitecoin.MimbleWimble.Tests/block1.txt new file mode 100644 index 0000000..efa34da --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/block1.txt @@ -0,0 +1 @@ +0000002037C3D1A8A8DE1E51AD8D3BB9866671D4994611EF2B3E561B80CF96EC0FEAC6B9AFA64AA0BA761A131621B83C10811741CE75017D2CEBC23EB05FAB305011D735FE6A3A65FFFF7F200600000003020000000001010000000000000000000000000000000000000000000000000000000000000000FFFFFFFF0502B0010101FFFFFFFF02140B824A000000001600141500D9EB709B25A56B523104D883E9F38D8E346E0000000000000000266A24AA21A9ED260AD835EBC9334E20017EA11BBAD893D8E9EEF1A3AA283A50363C55B185B641012000000000000000000000000000000000000000000000000000000000000000000000000002000000000101D58B7B0D601DF7D1C567FBE8F7B19B34F8583A12CC7C632FF03FF1FE938114590000000000FEFFFFFF01A872052A0100000022592008A5CCDDDE3AD474194F0540CCCA9A5BD74265DECB8AC200A515782521029C6702473044022066E2DD706A8AF63D25D9F21DC5C8842836D9F508EB423DB614169946315A90E702200614568C1778DCE3567DEC60FE6060BA9FDE75733C9CBEB72389223FA527876601210324B1C90D01D167AA88DAE5447B1B4E479AD8228BC174E2601B750A16EE94730EAF01000002000000000801D8D6C49E7D97BBFA9E1E6BCBD7E94D6BB0E773B27951055B0EE3A911351CA4F10000000000FFFFFFFF016C63052A0100000022582083A772E96B12E03CC234A48BDD7474749C9984032EE10DC72B048E17E579FBD1000000000001823066313BE96A43C32A20E7951EB52EDC7C15200505F59B767E542F82AA3EEDE40CF96D8F1200F9B4ED65960CE43FD8378319BEC86C46811CC71265BBF30B60F401AF2EDC674154FF129D9E826727ADA0828D3BA480924BBED84BF6DCAE2E1F1DB268BC457FCF5E99B14CC9B5477A9F67BDADAA1F6FEDCCF97A6E4490E6A1D5FBDE6CD43CF80643380C5D6AF43439E93DFC12554DDE4A414A878AE9BCDDAEC994B70201000208CDBCE43C8FAD17E523EE75030DE21719ED9ED418310B66281F59239FC0B652EF033C0A0B330F2D790C613717257EB5BFDCD835022C81E5ABEAD18F3CAAEEDC10DB0340F4B66050085A5F6F932310963A50A1DAA26F3F22EA4D256C85E4F38261936101028A709A126DCAB20650E51FF16AD992B1D0AD54A75A626DA30C91FA6D086D0C339C33455E35486AE54C1061B861BC68FF4F1AE4AF6063752F0B28E66AE26298FB8A89029DF6978578419D14DC0940710FEE13CAAC40788A25E6854B8E8AC88DB8F95DBAFD1F1F7BA0C82B4CB8EF4AA602A2AD806AFA1DE98B8B0835048BE996EE4CB189D2484820BE584E24E2A65AC8D662CF09C3F4A8D411FC8A12E3392B46F38871EA7FF85669F740E521B744B726749CBCC1C669917E8C067983AC76C097E7A52EEC7A3D7FE8CD12D4194CF6571050A54DC98DAA50DAA251E12389B43B6875120A662BDE26548B2BEEA716381D7D37828EF5228F3624BA08ACE75C36FC964D04127627A880895214918989A1B16ACBE442B3431E134DBDB71704F7527F693F09ABA9E235FF35A8BCF675140D680D88C874E102D13E814274151353B16580607F57E12CB09618FE21E2AA4C1FB55DFC382BE6A4D7C3E34221AD45E24C34D6BE95952F802E2EB8EFBE9CC2A060D05A3E68283BE2195183A7FA18CA1E6C96901B28FD0E93FE8BA4B7D46FCEEAF217D7B0D92E2B584AC6CF46629845032534D37F55422472BAFDD26291778E93E6363757002D2EE82FB055C42664DFC40C6771210000BA4212BA434865A3C0C7ACAC8AA5CB798353E34545CC69BB02E150C93E63DA80834A7564C82194A7916E4188D10D4CC8CDC866FDF269EEA879857A7410B6B643511E2953014B9D1F97B3079B7585FFEBD2E19C3F0B3D93E5525B71677BBFBF6919F219C7EE56B11ACBA306427C7A21968A18273849380E42CAB80B4C55A791D55A2D5409ADE5C983E8F46B59099C47E0F8E4A3FDCB39FB1D090465107FC7BDF3B05A7BDEFE35EC8C0F0548E48DD1DA0F5388A7605A723F7CC24B9FEC7C9063A329E39D22A0C3858B9067A177D7EC3504609309F1CC243C419AE5FAD0C719703D9A280ED5279958D9CC5AEEDC61F4D318F69030407C9815385BE28421DAA231F3E2E5FEA5EE0A76B8FE3102E6F84EF78F611E30385B36434828FC3C32B67473A466E4E70A2B5D211C4DEF56CE3D88379B32DCE70A4E75B53A4CEE53B9311936D6285BE6296D06CF5904165CEB30EE695E886C0902E9FBF36FED6F09E69851FF41562E0E2734A49E44E932823BBAFA56B13E2C1DB9EBCC0D27A0BD9F031429144BAAE14A98DA3ECE5E2098DB22E2CB56FA3533E094CA74014509A0FBD803000505E782A5FCEFEFB9C32677D8ABEE300EF5B0D36B55FD1F733356F168BE6A010283FC80195F400F46D1EC0D7BDB73CFE8875C20179E9DBEA7F5D334076AC24B8AA1736706E22AD93E9B7D2E19EE1C5D6A967D6A6B206969130DACEC029BB3A3A470A255B925C5632B7A2565C00E7BA6FAEF650CAD0D6B4B56209E3CA8B3F234E0884A44CE2A683A542F5C6BF0E84086690D66DA2768989E24D4070FB2BB47A68C5B12440F620E7BDB3385F6714734D1BE5F8C6DB069B26019F414942C78D364225DC4EFEAF98D38EB0B2C22371FC390144B2E88AE44C1B28A8058326E08880FAA9BB90CB6F6EF3A918D26D6237CEE30F719B8CC6FC4D0517EE844AECDB9B07A2B9972E41659942D6F6B8535B9EB0515AB8540BA12BAF1999264AA5249B8D2D0C4E5F000AA65638EE295A83B3979B518B1C366BA69BF3E3E48DC1B7E1F51C30355477CC927B1CABFE9FE346C815B7BBC7C2C27C0427551391D4A40BFC77CAA5CF44C75DC0AB0139029C7AA769F6685F79AE71F9DEAB4348F45348FCABC47D81E865A0410B61F2591E9288C33C03BF5A03D1C2F468002487A0B9A15938DFFC56F7F866A7683B792C16137B3781D2B7ECE381821ACC2CEC43CD3BD9F3001E5600EBDD925B1248F5B6BBD0975009FA98C1E74A78D24EC5593FD9F02E99FE2BC215D923150D126B7EE378EA18FCC2C174B7799568869A343B6419FC91CF13E280A05B119D84C35571BB60155CD524EA2D2DD382747AE21D2DD024277EF07DB4A92544C23BD7E8AD21941C7782D0FF10FCCAC8CAA425835FF76EC73FFCB20D08E4BF5B2486E29FED081D35B65D94CF50025EBE6BE072D85277CFFBD4BC9786D73CA17893E74C868A5F05D80DE06B647A6F40337350D080CF9EAAAB20DD6007F493E4B590AA11DAB394C8736083723BEF482E0F7723AE00299B70F319661B6A325AE25583D57C8F44BC0375A6AB0079AA30B003A1592AAF2ABBC5343BE56915EB01419226BBEA5B85C770E56C2D9D380368943B9FC5770BEB6E47CC30DDAA52F9958A6E844BDC48C9094DCF911E587591BEC71D0413F777A921F98D3027483C8888E0631CF9B32AD8E7169A2228EAD38A26FD4A0ED5E55A29845BC0F45633F5C4CF1FFCD1A36160CB30D5575A34E3B9037C27679CC7D2FF41BB1E17FAEFCF83A01139D3C91CF94E428021B768DDA5822104465A13DE66930A11C0CAC248FA6F41E505DA7325068703C0D0935652B6E778004EF3FD23ABB6DE5C2B7F489B8DF4B9AE88B680DC4130C271C09B2B09549CCF6BE6A924CF36CB0F04DF374F280CEE2465152BE3C87286ED643AC0728EF0029D17FD102E702499089D299806326D17A543F3E9495DA315F3D5ABD \ No newline at end of file diff --git a/tests/NLitecoin.MimbleWimble.Tests/transaction.bin b/tests/NLitecoin.MimbleWimble.Tests/transaction.bin new file mode 100644 index 0000000..89dfc33 Binary files /dev/null and b/tests/NLitecoin.MimbleWimble.Tests/transaction.bin differ diff --git a/tests/NLitecoin.MimbleWimble.Tests/transaction1.txt b/tests/NLitecoin.MimbleWimble.Tests/transaction1.txt new file mode 100644 index 0000000..425b4c3 --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/transaction1.txt @@ -0,0 +1 @@ +02000000000901d665c49a1cb1973e05f7f9bb307b51dddfa75b9b7e6bb0624b36a3218aa9ecae0000000000feffffff01aede1b000000000022592014140111b5f307eaf359aa33367b9edba6b4a42f37d2d4eace8aaf677d067fa20247304402201ec6d55a5abd355914cfde90af25ca150e831779b6d4ca090548dc568277af0c022012f12cd20bdfbb4bdc525c948cade0e286264264eb392128e0af2e69b3e4938d012102017ca8a145b76afcad8a86cce4ac0be0fc1e5df25926f1acd6a6ca4200ab6d7f0177d28dfd21f1d787fac83846578ee1e6e87d8a00514cd8b90dc886bdce5a014ffe4a2685f2d9e0bb2b1d4ad222f686f79d55de08431c1c3a34f9c26743e6ee3a0002091bb2f33342f32b66ed054b4c0e6c6dbe64df56ab597f1ed82220c3b1364005910365e8c9a1fd86ec9e9643bff2559f05da7dd2a5f6564fb3a606dd06ea8978f5f7020c36955520ad81ffd8e50874989908d8188e144b285a4c7f85db6596e4825d35010323f8931b16f40ed0bcaa810f9e75de86fcce5625f98f961939f8dc134e8a761a48c0352ed5e691c2b16dc834e5f060abcafc60032cec689c0ffe194720f2dd7755aaa7860ec2349d5803391bf1a0c3c78cbf307711ca467d2b1b95cb7a2caab04cce49946fcb27ea168465379b469daa81b637351e7ccedb37075e98d366b15e9726c47a613c25241a1277c03e98bfdfe142db61cba9d477d10c2401613374bef072728c89770b5ecbf9e8fa2c596f16a268dec531d84a6a8c69e16878e8125badf55089c658cfc3d61fd28791a913630b8e7e7bc3578e4bec658e5c3f0ac73d4cb82d9a7e1fb5de973c8274fd39a8289a19dd921e4099efad5b2aea0d166267df82e5f74142dc2d4b20ee5a68464b1cae222233d9bec84860e8c1b48e068ee3217d49b20ea5518da3f02cfd33741fb8c07706350e1a846d652d55ff2121ea199b2c3b3ebe7c4706a401563f3af44045bc3406e8ae0d9c7a48eebdb54dde6e89c1e08a1634ef7af7876823f8428db3429bbc3a1993599e8a9ddf33861b1206b4d8eece118446feaea7af12ec73d0cec9f3d4146cb87f6c802403bb00207a9e2adabfa77f8c56eafbfe2a87e4b3d7fd0f11e6444233de2eff8c709fbb68723d422bb0eed830a2ae2072691ac58c527b446833d698768664add28200fd6cbb01609f0d4373a7135f4c691ff4ad8e72716d3ac8b9513513c60451fdcb40a19139a1aadf9488080bb3ab39362d2d7c1eac1bdd9882f28594c6f5d42789931458e5fd3526229ab44251166cef236b8a4ae398817638b89703ea683b182507901c8a430d83a5eca64d3b348c308bd6506fad9682ee9f93ccc5a54aac1f1ba2917b431f0a4f4a925afadfeb2d27abe2a6ab288f87579b61b72ba7e293340a57382ddb096da98a4f12b7ab60f36195357850c50f36b21c89967d854b9edaaf42bd47d22c6f8818de14a793695a8765f399ed9d4f61305d272033d2c588839b2d92e1b68d612474d1f97801e530f203218eb8bd0cbf36f398a87e0469de27311eb5f7d9c2a4f5736d1ec837435d2def728a22b975be9275ed598caca2f830712cac75036c5ebdf80b04d9b2d81bdd8fb4c7b5df57a3057b951007cf22147e554c08e73c55b1f635693d203aa71b7cf80b787202fee627a400480d798144f1e6749603336e99bcfa61beae867c0de733edea6c82e645b2a955dd1861da341abbe0445703e4c2885f9062b9105a88e3b1e3ca17d38e91406042edb2d75927c680e577884f0103a19f231b7e481fc3b4cff79f38c4da93892b11ffaf9677caa20daba6c9828a6bf835801abfae22ecff4b4dfa2fbbb3cfa46696e17ddea93165578121b4434779f6157d6f583341c9253ee6e87e82a9d1bf4ce6e611337199392938f814526b48bdebb92feb28b8dcb8dd3c6051cb601d914a03c824a9aa6e700e3571b47d92341b2c5eadd8c4474f3d105767823ed236925f49d1e4b431d8f771a07a170de943c2dbd40afae10d3332cfeff06b9c174c265a6dc3f764cf6a65c848b202c2f2daf9770631bac4c3e14ebfc4800459a9bc9689ed5dffc1ee2d604a183b4089dada6ac868d3c3558aa9fc65103514ec4f8796a38e2fce536d539e676ac613e71b2fba6d19449a32dc2a444818dee211e88830b4fb3f4c947deacc78b51dac60125f9653fa328b5a129fcb8fc34229cc2f1ff6118a32916dc8db89b86517e2ed1b902b71707b3c7919149f2fc758fbb88b46ab70a68b44c225822220f64576e7aecbc3876b9192f6f11d194bc191a52df04a78cbcbd8c031875d8bf11a20c4968daae0994989e685ff22d9f8437886dad94fdecf52fec20be87f8cf2c702aab79edab3a69db2e83f41b8fe52ccec2efd36ac7c245d32432fc7ee6da96810bf66769c6ba79f79d6a372fce410e9f76d54a5a594ca458803e826b2eea081a0f8a9a4639abc928d23819336475fd6003f4efda6b224f6333ff866b9dfc92a3672cacd6417ccf28711213e9288007a5bafaa264bb921ede0167ee87f405b57bf489d6b67a6564b92f83489e1ab9d4bbf473b7e1e7d3fe5dec9e8a4552d4fcf1ddf011c94c8b8606580de77b7935f2d381f9cfa49c0a00738e3408cea81f9a9c10823cf04b3d14e4ab80137391d7cf46f200b708b8f2291538d32dee8fc3d9d202d5f2c1a34476bf1cd10816d36781d4a8c4609954006f013d59d21364ad2219c6984540ca2cf51be7c94444fee63fc190f6577fc5324f4948d827467a503ee1c27be5c5e3b80b5522d1a9acf3dffd42f4487b01f4cc491e7692a65f3869a5e7ca994704ef5040bf6e8178d18d23829517315e8dc85d23334a7e68bbddef7f03c8b790832b80d3a0b22101341d8ffac09f659363f86e9fa14e383b00612d6ae5301139d3ceebc2e025d13b0d35616a626f43aae0308146dd29b0744e631443c9627bbba018e3dd602095bdd6e21fed8dbad135a902f8f5e4415c91145127dc13b9bb9b44344aa0cd287b2d3a8a4f39100b28725f606d9cb42c9befa4f7c9c98cc42517f7d1d89376734d23ba528fea7bd68a44483cd9607447c1f9ee702f30e390bec7cf6c2dbae4ceb669d2600 \ No newline at end of file diff --git a/tests/NLitecoin.MimbleWimble.Tests/transaction2.txt b/tests/NLitecoin.MimbleWimble.Tests/transaction2.txt new file mode 100644 index 0000000..07eea0f --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/transaction2.txt @@ -0,0 +1 @@ +020000000008000001e7dbef8a58677854c27a3242e1ea9d2011abf79b2329b306094397d993261fa18ecb75b5d8e0c5acbb89a4303a09bc73727c73ab2f644d59bbc11bdf54b4d7bb0201267302c35d4641385f30a81399599f885de2c8f0e753524472384ad6820823bf098c433351f8ae899f8a0d439ab64dae8030d0c14514d42e2ccf9b20fce6e1427302971a22492ee986021d8650778fa4085e773d07ece3b7f9083943bfcd68236479028c9168c280a2d7dc01d7dd69c7ab1c1b22b9498be2d31b17977296fd170eff7ce1327b4d7f672a19eb75f1c6ba384ced58e217544ff38b52b181408920645f45eda18815c49e69b1ebe53b0ba16f061a04326f1b0f0db237161b625cedaa0a5d01a76d4401acd07694ab70e34d7f6246901c4c15cda33d3327e182e704b88615e7091bb2f33342f32b66ed054b4c0e6c6dbe64df56ab597f1ed82220c3b136400591020c36955520ad81ffd8e50874989908d8188e144b285a4c7f85db6596e4825d35039e9d625e1b165002a805901c5a6556857e1db4445287674fe4a018aeec93e762dcdbe9aec64ef7441e25dad5d1781d41c7379f738861004c5ee002cdb12ea8472c9fa4c5c7653a527ed99df207b53520b8966b6a38ef5c925af257492caef7f002095948146cddd5f7011c7d585f05174572b6d154177bcfed1aafb3ac9ca5766d59024559786e802d0e98bcdae16a74e9d62dd91b4b17829e1a0f41b1a16e359f93e503d1fba9bc399ada2ec21502e6109b546f05580ca6e4698d1797c5e1f36860ccd60103a57997fc39e5da52b60d36f529900a32036239e41b0e91d110500364c2c15ef7d3812790acce4e797c12e0ee7a9c7ff7653281422f8605615a6119fb875e493a9e24291f4ee28da54390f52ba2d41bceab2a7ab025490cd8aeaca9013612112bb2803402a86b6cece06c57a7b4f2cb302faa21b79afd97f2e107fcb611bd1e582f28795484bd3d0b6948ba4b1b37822d04b0ed8657397188e3e989cc0f1cd26d1540c052e6ae842f16d673b36562ca5bb148cb69bd074612651b92c43b26de89eed229b495a7b124059ef902d078a0701bb4ff157f17790f59bd7fbc99810f298c0a5138f40273d474be35544ba8ac71436bd4c0edc5adc78fc73650400af7cbfaa06014d1cc65ad3c4e3ba401fd929c55d47b9d601a30dc057d620afb2f5c58684bcfb7485232a848c58691083ee4edaa746f9c2d77105ed08fafc3454d2b4742548a766099dab763df282036ae9b6f2683349f62f6b940c2d81acf015a6852bbfcd9723f5b2cbdb6300333d1146ee1dbbf6e82ca4fe1b1d951b9b197079c12f6197fc587df5bf8f886d930b8656aeb3bd9a4b6d650c70831a897036487a6843932cdf066f2feb8fe0af22efe2892ea44382c3b89070ae696d92aaf637dd388acbf9ca62778a3e540ce6558823218361ea0179b5f0734855cbf1cca7999d6056c083d15259d9c662d699e455f100e5b99f3af11a358c9f80e8387ce4e6ac07521e5f57617a34d2723079ff2e6ba25b0bc4b250b905a13c4209c80c232bee6016ff0ec7133a8c2fd7ee7769664b0f5e2ac859f257a89252a8d23e10a514d8ccfd80a0aa0701859a0dfb2a6b31749e0acb9d6155afafc6b647af733fa83f9e21cf93973c6f514189eca7290ed34ebbe3d90cd9af7d501876b8e8e66b54cfb848cdb08b36b34e1832f9f56e8d8757bc4ddca141fdc448422c055ab16f93c82d88852aad16aaef04fbde891c061fe9c2a1a0ae7f1ac4a299b7bdec6defe57df037a9b183c7b589ad6a9908ab31cde6b0aa5e2cac63c64734607ca0132b9d4f928d3d9941668139aee53748349666d3940f98e72de6d4392bb3789c6740b98a2f0ee72d50b42441ad67eea0fc5ab3ab5dc0034a46bb3606e69fa74327d4909db055f5b120d39577b22410f9bad2f4718809167729104ceeab6bd36cd3b051a034740c568d1b0cd0a85ae42f35d6d2e988c5623c6c5688d9b1b6950325ebf491f02adee1a1fee40f817447a62971a313e52608c41a19a8b5b748f9a6c99b06dabb301030fb6546f8d9bc4537b78b2c49393f4696b5d3a2a8b233b6997b38b51255f394f3e7213f1f34da5a1edff4302fa7cf6de3eb71d0ae0aa47ccfcf55ea847cd61550cbe662b854482118615bdabc6de305209933c78da96590d995fcc54482bd64883d9cbfbb2b57c510ea3a6f373c1459283c05f88f733ad28930c9a14e160613007d37afeef95ce242d3876cc00a174a177707e34b6d95ce4cf8a3e7895a4f5436f93e33cd368df79563f0399014709ed338eb3ae1449f8b89be10c52732640766db53dcb19a38fc50f0aa6debdbd425570730325854dd66e23f1903b7580b31f0ba7d9c748b74c3cab09e090cfd38164714e2de4ecabcdec16a73b3ab75ec037c4518fa3763b5c644822f68df003bba17ef13479f8ffadd375725ed2941267b9be8af449fdd8f325114438864a62dc548e7f8a0b2b4dbe4c71a31959dcc9b935b4eb6a89fcfd173fa37dcf61cea9648642754ddb2e5e89286905ffbaed3d34438bac9c092e4457287c67acafe40d7654aee0f17b555bb54f6d90c47a93e63280f0316606e4ad36071f3cfc3087eef6a7ef84e4edad347b4e215b1b03cffe4b1b3ae8aade5657388158ab11bb5c6224014f846eabfa46d982cc4142696534ba1869bd69eb9c6395122898893cce12509c1144d63ebdb2c104f8315c9eab1b799a6ac52e00e87757240f5e1fa0bbc65cd2fbf1a3e5b70c9b929808523530171b444d585e9adcaa312d3402b34cb8e7528833d45b4d21c13ee7c61bdcc54a5b2081ae75692d659399bf3a8c32db36efa20cf97412cce8850e7d084c2a8301fc88a81ab6544ce749b7a001f00a196b119632f7a7142863ea7930506d15664da81682523db690329efd7073dd13dcdd1067014d3dd81b8b7920be50fb8c1529486af12185ee7241223b194b6c11b35a069596778a606a7d8716c72fa8225c5977fa8e8ce9e7d71389cd1ef49dd9177326dfd7047bce9199ac1d0fb779f31c786ef70f69fc5fc7101cbf64397cc286464cde2452ea14d44cb34885428914e134efe7461a9875a5f707bb53fe1ab17a7f19c5b33ed71ccae4a202cb5ab7143c2eda80ae0dd02b07e9c04a02e12a976c2631ac6ff301c03ab09e84a25f21255a01119d3c020690ff6c060ba44eb4755f5c187a7c9ac91e12cc2c773f21021f0c7b09c45ee108994019f58550796b53259f72d4093e5186cf0bf6a4945edf746c8525ffebcb2679c7e55683617fb14a75ff139873a47a0176c10c02067f22d55bcb8446ce09118292d32fea60a7420b4908ad61565ee24a31b0688a9805146cdbfc694629f9d4b69d2600 \ No newline at end of file diff --git a/tests/NLitecoin.MimbleWimble.Tests/transaction3.txt b/tests/NLitecoin.MimbleWimble.Tests/transaction3.txt new file mode 100644 index 0000000..69dcee4 --- /dev/null +++ b/tests/NLitecoin.MimbleWimble.Tests/transaction3.txt @@ -0,0 +1 @@ +020000000008000001659215067dac03a73dd5414802a00f3b0c25c427aee38c97cea665964d8dd121482181fec4811e9a50b171bce63a360d30348c0df4476c7ab03fa417180f4bdf0101f1ea3367cf30c49fedd1be1426b05b629c468075d4d0a300fbec38b97d99a99b09db055f5b120d39577b22410f9bad2f4718809167729104ceeab6bd36cd3b051a02adee1a1fee40f817447a62971a313e52608c41a19a8b5b748f9a6c99b06dabb30282d62592524a268cfccbc1b866a1259b62d4a912c7d1683c2d9f517329c3dad79a7385472cb4b825e875ea35362d39479ca48607542fec2570d85dc17a7512802c8b0a6c5a874ba6db3da4af999e069c6be98b91b6e558b7698a67a259707e230109d15c178012e36fdd97b6aca81a11c5773cdcee9353d1443f99c049e020e901d002c6927b761a937858bb7b0f7e876be70df983bf0f316acfebc70d14aa1392a0010305aaf53af52e1aece90051d6926f247caa00c3b65f761ed581fae199afb4b9c30103566ce3b005802e82dfb94443d556a738cec2519b67b44b66b06fc0038b691be830fa36a0f6b5f1ba39721a676100b6dbab8e211479230fb007107590ee274786366015a10b6bce52073d5867c4e0c5fd9eda32657d4d541dcb699522f30c0234209b84621c03d6033aa53eaeaef6c73b0fb8d076f80b34ebf20bfb06f4369f7a0d0100b936f289575a813dce7c61e72c97925b4e75594a72106068393596e2bf30f62d0d9aa454cfc8a8a8468a8d5f2ce6ce518629638dc8fd9436327f1d0a4476f37330101da8fc3ee93bc6fa830ae6059e6cf826fc76e8610f563d0dc2ed8fcc81162f7735ce1ab055ee5a6f23fea46fbf7b43a368c9d02e9e41158d4456290789f733e091d818dd736e1a34e2cc1e2c3d5e93f83db851f4d22f0919882e6e8a014fcf381dca6c9006585a4ef3d7ed61b0bc9d037b0199cf93283d91140f5aae1f73ea976ac44bd5338a3e64037775f0419f6ef4aed3ce39c3b07b8d07bf75d0bed9fb32bd68d710b63b1115f8c19b2c4d5d248179e710a2f283f3bb946fb6edd4da589826ed7fd32e7a73d43510d28fb02ef3fcd0a20b7ef72500627557f9ede13df22d0f1633a3f29e560160e80f9931ebfd27e11f8699a69c20d4acd7cb860dd01c35c4c41734155bd38c5dd573c4944ce04dd598b777e077d07d022f52f1ee402ba4ce7923405330d51a06e5a9fb9f45218248a770be3fbbc0ce8fcb72e148ea8006e633ae92d812aa760f24663864cffc5ac44d2078acda93c2e730f5570e6a748dd3c75b628479d476c0fc1b401c273b856c7354a51273c1241e712517071160647ae7dcb30a2f5cc7e428497fbd937541ef54a3f65d2da7d4aebe649cf8444a55d63e74264243eb22b80ce1626aaed689eec998889a798449adcd590cc68f08e42332ffb5dbb1ed1f6cdfbd5d2fddcdfaba39b6e5e3a0abc3977a92fab7e96f054794b6261176eed49268b56e887c80c1698b5b2d60294c3d90857c1c39f0770c32a695cf7bb316511a3005dcd482bf123c42a8434286052f80e90f3a1ce25a75aeb2093bd1743e6d4b7ef3213c60a3b3c003831d18f76669d6f55ad0ac2163a8303c421312b0f69f51f2eb49c2529b9be912918a6bdd000115924e0184f852160014be09d3909f28efd4e2969c793178c808742dc70b0299c8ac59d2e8d6c36f6b51714d4690605c210d68a4cee4c1964efc4e482fbe07090a504f8a332b8138909301c23a16d0b42d48e146efe44629b792071731fc550b79bf292d4297304bb9a8077d94a12167cf973a285b5ba45a1e55fe9065a0d6d2d4791a191c8a6399b4df45c613034b5b20b90cfb52d9bf3e85b7b58e8b513a38be9d2600 \ No newline at end of file diff --git a/tests/NLitecoin.Tests/NLitecoin.Tests.csproj b/tests/NLitecoin.Tests/NLitecoin.Tests.csproj index 9d5c191..3fa854e 100644 --- a/tests/NLitecoin.Tests/NLitecoin.Tests.csproj +++ b/tests/NLitecoin.Tests/NLitecoin.Tests.csproj @@ -10,7 +10,7 @@ - +