From a95defd4d0fb394f54b17d9254935528348aa1e0 Mon Sep 17 00:00:00 2001 From: Nikita Orlov Date: Mon, 2 Sep 2024 09:13:23 +0200 Subject: [PATCH] Draft: All nuts types + methods + hashing algos of btc (#5) * bech32 impl * bech32 decoder/encoder * recover id + recoverable signature for secp256k1 * very raw lnbit backend and NUT04 * rm test * nut-04 without testing * RIPEMD 160 BIP32 secp256k1 updates * base nuts types implemented * remove old code + bip39 min impl * add test to github step, fix benchmarks * add bip39 mnemonic to nut13 --- .github/workflows/check.yml | 3 + .gitignore | 7 +- README.md | 5 + build.zig | 49 +- build.zig.zon | 4 + src/bech32/bech32.zig | 554 ++++++ src/benchmarks.zig | 78 +- src/core/amount.zig | 194 ++ src/core/bdhke.zig | 252 --- src/core/bip32/bip32.zig | 774 ++++++++ src/core/bip32/key.zig | 12 + src/core/bip32/utils.zig | 182 ++ src/core/bip39/bip39.zig | 352 ++++ src/core/bip39/language.zig | 2124 +++++++++++++++++++++ src/core/bip39/pbkdf2.zig | 143 ++ src/core/blind.zig | 14 +- src/core/dhke.zig | 200 ++ src/core/keyset.zig | 356 ---- src/core/lib.zig | 13 +- src/core/nuts/lib.zig | 17 + src/core/nuts/nut00/lib.zig | 536 ++++++ src/core/nuts/nut00/token.zig | 58 + src/core/nuts/nut01/nut01.zig | 185 ++ src/core/nuts/nut02/nut02.zig | 297 +++ src/core/nuts/nut02/nut02_test.zig | 156 ++ src/core/nuts/nut03/nut03.zig | 59 + src/core/nuts/nut04/nut04.zig | 38 + src/core/nuts/nut05/nut05.zig | 147 ++ src/core/nuts/nut06/nut06.zig | 234 +++ src/core/nuts/nut07/nut07.zig | 88 + src/core/nuts/nut08/nut08.zig | 24 + src/core/nuts/nut09/nut09.zig | 46 + src/core/nuts/nut10/nut10.zig | 171 ++ src/core/nuts/nut11/nut11.zig | 734 +++++++ src/core/nuts/nut12/nuts12.zig | 222 +++ src/core/nuts/nut13/nut13.zig | 270 +++ src/core/nuts/nut14/nut14.zig | 113 ++ src/core/nuts/nut15/nut15.zig | 30 + src/core/primitives.zig | 57 - src/core/secp256k1.zig | 334 +++- src/core/secret.zig | 48 + src/helper/helper.zig | 149 +- src/lib.zig | 12 +- src/main.zig | 4 - src/mint.zig | 19 +- src/mint/config.zig | 33 - src/mint/database/database.zig | 366 ++++ src/mint/lib.zig | 4 - src/mint/lightning/invoices/constants.zig | 12 + src/mint/lightning/invoices/error.zig | 25 + src/mint/lightning/invoices/invoice.zig | 654 +++++++ src/mint/lightning/invoices/lib.zig | 4 + src/mint/lightning/invoices/ripemd160.zig | 263 +++ src/mint/lightning/lib.zig | 4 + src/mint/lightning/lightning.zig | 61 + src/mint/lightning/lnbits.zig | 301 +++ src/mint/mint.zig | 130 +- src/mint/routes/default.zig | 150 ++ src/mint/server.zig | 41 - src/mint/types.zig | 96 + src/mint/url.zig | 17 + 61 files changed, 10582 insertions(+), 943 deletions(-) create mode 100644 src/bech32/bech32.zig create mode 100644 src/core/amount.zig delete mode 100644 src/core/bdhke.zig create mode 100644 src/core/bip32/bip32.zig create mode 100644 src/core/bip32/key.zig create mode 100644 src/core/bip32/utils.zig create mode 100644 src/core/bip39/bip39.zig create mode 100644 src/core/bip39/language.zig create mode 100644 src/core/bip39/pbkdf2.zig create mode 100644 src/core/dhke.zig delete mode 100644 src/core/keyset.zig create mode 100644 src/core/nuts/lib.zig create mode 100644 src/core/nuts/nut00/lib.zig create mode 100644 src/core/nuts/nut00/token.zig create mode 100644 src/core/nuts/nut01/nut01.zig create mode 100644 src/core/nuts/nut02/nut02.zig create mode 100644 src/core/nuts/nut02/nut02_test.zig create mode 100644 src/core/nuts/nut03/nut03.zig create mode 100644 src/core/nuts/nut04/nut04.zig create mode 100644 src/core/nuts/nut05/nut05.zig create mode 100644 src/core/nuts/nut06/nut06.zig create mode 100644 src/core/nuts/nut07/nut07.zig create mode 100644 src/core/nuts/nut08/nut08.zig create mode 100644 src/core/nuts/nut09/nut09.zig create mode 100644 src/core/nuts/nut10/nut10.zig create mode 100644 src/core/nuts/nut11/nut11.zig create mode 100644 src/core/nuts/nut12/nuts12.zig create mode 100644 src/core/nuts/nut13/nut13.zig create mode 100644 src/core/nuts/nut14/nut14.zig create mode 100644 src/core/nuts/nut15/nut15.zig delete mode 100644 src/core/primitives.zig create mode 100644 src/core/secret.zig delete mode 100644 src/mint/config.zig delete mode 100644 src/mint/lib.zig create mode 100644 src/mint/lightning/invoices/constants.zig create mode 100644 src/mint/lightning/invoices/error.zig create mode 100644 src/mint/lightning/invoices/invoice.zig create mode 100644 src/mint/lightning/invoices/lib.zig create mode 100644 src/mint/lightning/invoices/ripemd160.zig create mode 100644 src/mint/lightning/lib.zig create mode 100644 src/mint/lightning/lightning.zig create mode 100644 src/mint/lightning/lnbits.zig delete mode 100644 src/mint/server.zig create mode 100644 src/mint/types.zig create mode 100644 src/mint/url.zig diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4d9bfbe..8947eee 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -45,5 +45,8 @@ jobs: - name: Build and Run run: zig build run -- info + - name: Unit testing + run: zig build test --summary all + - name: Run benchmarks run: zig build bench -Doptimize=ReleaseFast diff --git a/.gitignore b/.gitignore index 9d0b999..0133c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,9 @@ Cargo.lock **/.zig-cache **/zig-out -**/benchmark_report.csv \ No newline at end of file +**/benchmark_report.csv + + +.vscode + +**/.DS_Store diff --git a/README.md b/README.md index eb116d0..8b1acbd 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ Coconut 🥥 is a Cashu Wallet and Mint implementation in Zig. +## Test +```sh +zig build test --summary all +``` + ## Build ```sh diff --git a/build.zig b/build.zig index 708c20c..0d871a7 100644 --- a/build.zig +++ b/build.zig @@ -12,7 +12,7 @@ const external_dependencies = [_]build_helpers.Dependency{ }; fn buildSecp256k1(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) !*std.Build.Step.Compile { - const lib = b.addStaticLibrary(.{ .name = "zig-libsecp256k1", .target = target, .optimize = optimize }); + const lib = b.addStaticLibrary(.{ .name = "libsecp", .target = target, .optimize = optimize }); lib.addIncludePath(b.path("libsecp256k1/")); lib.addIncludePath(b.path("libsecp256k1/src")); @@ -21,6 +21,9 @@ fn buildSecp256k1(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std defer flags.deinit(); try flags.appendSlice(&.{"-DENABLE_MODULE_RECOVERY=1"}); + try flags.appendSlice(&.{"-DENABLE_MODULE_SCHNORRSIG=1"}); + try flags.appendSlice(&.{"-DENABLE_MODULE_ECDH=1"}); + try flags.appendSlice(&.{"-DENABLE_MODULE_EXTRAKEYS=1"}); lib.addCSourceFiles(.{ .root = b.path("libsecp256k1/"), .flags = flags.items, .files = &.{ "./src/secp256k1.c", "./src/precomputed_ecmult.c", "./src/precomputed_ecmult_gen.c" } }); lib.defineCMacro("USE_FIELD_10X26", "1"); lib.defineCMacro("USE_SCALAR_8X32", "1"); @@ -84,6 +87,16 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); + const zul = b.dependency("zul", .{ + .target = target, + .optimize = optimize, + }).module("zul"); + + const base58_module = b.dependency("base58-zig", .{ + .target = target, + .optimize = optimize, + }).module("base58-zig"); + // ************************************************************** // * COCONUT AS A LIBRARY * // ************************************************************** @@ -103,6 +116,34 @@ pub fn build(b: *std.Build) !void { // running `zig build`). b.installArtifact(lib); + // ************************************************************** + // * CHECK STEP AS AN EXECUTABLE * + // ************************************************************** + // for lsp build on save step + { + const exe = b.addExecutable(.{ + .name = "coconut-mint", + .root_source_file = b.path("src/mint.zig"), + .target = target, + .optimize = optimize, + }); + exe.linkLibrary(libsecp256k1); + exe.root_module.addImport("httpz", httpz_module); + exe.root_module.addImport("pg", pg.module("pg")); + exe.root_module.addImport("zul", zul); + + // Add dependency modules to the executable. + for (deps) |mod| exe.root_module.addImport( + mod.name, + mod.module, + ); + + // These two lines you might want to copy + // (make sure to rename 'exe_check') + const check = b.step("check", "Check if foo compiles"); + check.dependOn(&exe.step); + } + // ************************************************************** // * COCONUT-MINT AS AN EXECUTABLE * // ************************************************************** @@ -115,6 +156,7 @@ pub fn build(b: *std.Build) !void { }); exe.linkLibrary(libsecp256k1); exe.root_module.addImport("httpz", httpz_module); + exe.root_module.addImport("zul", zul); exe.root_module.addImport("pg", pg.module("pg")); // Add dependency modules to the executable. @@ -172,6 +214,9 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); lib_unit_tests.linkLibrary(libsecp256k1); + lib_unit_tests.root_module.addImport("zul", zul); + lib_unit_tests.root_module.addImport("httpz", httpz_module); + lib_unit_tests.root_module.addImport("base58", base58_module); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); @@ -196,7 +241,7 @@ pub fn build(b: *std.Build) !void { }); bench.linkLibrary(libsecp256k1); - bench.root_module.addImport("zul", b.dependency("zul", .{}).module("zul")); + bench.root_module.addImport("zul", zul); const run_bench = b.addRunArtifact(bench); diff --git a/build.zig.zon b/build.zig.zon index 73c3393..9af8adf 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -39,6 +39,10 @@ .url = "https://github.com/karlseguin/pg.zig/archive/1491270ac43c7eba91992bb06b3758254c36e39a.zip", .hash = "1220bcc68967188de7ad5d520a4629c0d1e169c111d87e6978a3c128de5ec2b6bdd0", }, + .@"base58-zig" = .{ + .url = "https://github.com/ultd/base58-zig/archive/e1001fbe8b41eed36d81e37931ada66b784e51dc.zip", + .hash = "12206e5050a03cd9dcb896781de0cf541081488006532675371653f61d00c1f27433", + }, }, .paths = .{ "build.zig", diff --git a/src/bech32/bech32.zig b/src/bech32/bech32.zig new file mode 100644 index 0000000..e9b4aa8 --- /dev/null +++ b/src/bech32/bech32.zig @@ -0,0 +1,554 @@ +const std = @import("std"); + +const Case = enum { + upper, + lower, + none, +}; + +/// Check if the HRP is valid. Returns the case of the HRP, if any. +/// +/// # Errors +/// * **MixedCase**: If the HRP contains both uppercase and lowercase characters. +/// * **InvalidChar**: If the HRP contains any non-ASCII characters (outside 33..=126). +/// * **InvalidLength**: If the HRP is outside 1..83 characters long. +fn checkHrp(hrp: []const u8) Error!Case { + if (hrp.len == 0 or hrp.len > 83) return Error.InvalidLength; + + var has_lower: bool = false; + var has_upper: bool = false; + + for (hrp) |b| { + // Valid subset of ASCII + if (!(b >= 33 and b <= 126)) return Error.InvalidChar; + + if (b >= 'a' and b <= 'z') has_lower = true else if (b >= 'A' and b <= 'Z') has_upper = true; + + if (has_lower and has_upper) return Error.MixedCase; + } + if (has_upper) return .upper; + if (has_lower) return .lower; + + return .none; +} + +fn verifyChecksum(allocator: std.mem.Allocator, hrp: []const u8, data: []const u5) Error!?Variant { + var exp = try hrpExpand(allocator, hrp); + defer exp.deinit(); + + try exp.appendSlice(data); + return Variant.fromRemainder(polymod(exp.items)); +} + +fn hrpExpand(allocator: std.mem.Allocator, hrp: []const u8) Error!std.ArrayList(u5) { + var v = std.ArrayList(u5).init(allocator); + errdefer v.deinit(); + + for (hrp) |b| { + try v.append(@truncate(b >> 5)); + } + + try v.append(0); + + for (hrp) |b| { + try v.append(@truncate(b & 0x1f)); + } + + return v; +} + +/// Generator coefficients +const GEN: [5]u32 = .{ + 0x3b6a_57b2, + 0x2650_8e6d, + 0x1ea1_19fa, + 0x3d42_33dd, + 0x2a14_62b3, +}; + +fn polymod(values: []const u5) u32 { + var chk: u32 = 1; + var b: u8 = undefined; + for (values) |v| { + b = @truncate(chk >> 25); + chk = (chk & 0x01ff_ffff) << 5 ^ @as(u32, v); + + for (GEN, 0..) |item, i| { + if (std.math.shr(u8, b, i) & 1 == 1) { + chk ^= item; + } + } + } + + return chk; +} + +/// Human-readable part and data part separator +const SEP: u8 = '1'; + +/// Encoding character set. Maps data value -> char +const CHARSET: [32]u8 = .{ + 'q', 'p', 'z', 'r', 'y', '9', 'x', '8', // +0 + 'g', 'f', '2', 't', 'v', 'd', 'w', '0', // +8 + 's', '3', 'j', 'n', '5', '4', 'k', 'h', // +16 + 'c', 'e', '6', 'm', 'u', 'a', '7', 'l', // +24 +}; + +/// Reverse character set. Maps ASCII byte -> CHARSET index on [0,31] +const CHARSET_REV: [128]i8 = .{ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, + 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, + 6, 4, 2, -1, -1, -1, -1, -1, +}; + +/// Error types for Bech32 encoding / decoding +pub const Error = std.mem.Allocator.Error || error{ + /// String does not contain the separator character + MissingSeparator, + /// The checksum does not match the rest of the data + InvalidChecksum, + /// The data or human-readable part is too long or too short + InvalidLength, + /// Some part of the string contains an invalid character + InvalidChar, + /// Some part of the data has an invalid value + InvalidData, + /// The bit conversion failed due to a padding issue + InvalidPadding, + /// The whole string must be of one case + MixedCase, +}; + +const BECH32_CONST: u32 = 1; +const BECH32M_CONST: u32 = 0x2bc8_30a3; + +/// Used for encode/decode operations for the two variants of Bech32 +pub const Variant = enum { + /// The original Bech32 described in [BIP-0173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) + bech32, + /// The improved Bech32m variant described in [BIP-0350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki) + bech32m, + + // Produce the variant based on the remainder of the polymod operation + fn fromRemainder(c: u32) ?Variant { + return switch (c) { + BECH32_CONST => .bech32, + BECH32M_CONST => .bech32m, + else => null, + }; + } + + fn constant(self: Variant) u32 { + return switch (self) { + .bech32 => BECH32_CONST, + .bech32m => BECH32M_CONST, + }; + } +}; + +/// Decode a bech32 string into the raw HRP and the `u5` data. +fn splitAndDecode(allocator: std.mem.Allocator, s: []const u8) Error!struct { std.ArrayList(u8), std.ArrayList(u5) } { + // Split at separator and check for two pieces + + const raw_hrp, const raw_data = if (std.mem.indexOfScalar(u8, s, SEP)) |sep| .{ + s[0..sep], s[sep + 1 ..], + } else return Error.MissingSeparator; + + var case = try checkHrp(raw_hrp); + var buf = try std.ArrayList(u8).initCapacity(allocator, 100); + errdefer buf.deinit(); + + const hrp_lower = switch (case) { + .upper => std.ascii.lowerString(buf.items, raw_hrp), + // already lowercase + .lower, .none => v: { + try buf.appendSlice(raw_hrp); + break :v buf.items; + }, + }; + + buf.items.len = hrp_lower.len; + + var data = std.ArrayList(u5).init(allocator); + errdefer data.deinit(); + + // Check data payload + for (raw_data) |c| { + // Only check if c is in the ASCII range, all invalid ASCII + // characters have the value -1 in CHARSET_REV (which covers + // the whole ASCII range) and will be filtered out later. + if (!std.ascii.isAscii(c)) return error.InvalidChar; + + if (std.ascii.isLower(c)) { + switch (case) { + .upper => return Error.MixedCase, + .none => case = .lower, + .lower => {}, + } + } else if (std.ascii.isUpper(c)) { + switch (case) { + .lower => return Error.MixedCase, + .none => case = .upper, + .upper => {}, + } + } + + // c should be <128 since it is in the ASCII range, CHARSET_REV.len() == 128 + const num_value = CHARSET_REV[c]; + + if (!(0 >= num_value or num_value <= 31)) return Error.InvalidChar; + + try data.append(@intCast(num_value)); + } + + return .{ buf, data }; +} + +const CHECKSUM_LENGTH: usize = 6; + +/// Decode a bech32 string into the raw HRP and the data bytes. +/// +/// Returns the HRP in lowercase, the data with the checksum removed, and the encoding. +pub fn decode(allocator: std.mem.Allocator, s: []const u8) Error!struct { std.ArrayList(u8), std.ArrayList(u5), Variant } { + const hrp_lower, var data = try splitAndDecode(allocator, s); + errdefer data.deinit(); + errdefer hrp_lower.deinit(); + + if (data.items.len < CHECKSUM_LENGTH) + return Error.InvalidLength; + + if (try verifyChecksum(allocator, hrp_lower.items, data.items)) |v| { + // Remove checksum from data payload + data.items.len = data.items.len - CHECKSUM_LENGTH; + + return .{ hrp_lower, data, v }; + } + return Error.InvalidChecksum; +} + +/// Encode a bech32 payload to an [WriteAny]. +/// +/// # Errors +/// * If [checkHrp] returns an error for the given HRP. +/// # Deviations from standard +/// * No length limits are enforced for the data part +pub fn encodeToFmt( + allocator: std.mem.Allocator, + fmt: std.io.AnyWriter, + hrp: []const u8, + data: []const u5, + variant: Variant, +) !void { + var hrp_lower = try std.ArrayList(u8).initCapacity(allocator, hrp.len); + defer hrp_lower.deinit(); + + hrp_lower.appendSliceAssumeCapacity(hrp); + + _ = if (try checkHrp(hrp) == .upper) std.ascii.lowerString(hrp_lower.items, hrp); + + var writer = try Bech32Writer.init(hrp_lower.items, variant, fmt); + + try writer.write(data); + try writer.finalize(); +} + +/// Allocationless Bech32 writer that accumulates the checksum data internally and writes them out +/// in the end. +pub const Bech32Writer = struct { + formatter: std.io.AnyWriter, + chk: u32, + variant: Variant, + + /// Creates a new writer that can write a bech32 string without allocating itself. + /// + /// This is a rather low-level API and doesn't check the HRP or data length for standard + /// compliance. + pub fn init(hrp: []const u8, variant: Variant, fmt: std.io.AnyWriter) !Bech32Writer { + var writer = Bech32Writer{ + .formatter = fmt, + .chk = 1, + .variant = variant, + }; + + _ = try writer.formatter.write(hrp); + try writer.formatter.writeByte(SEP); + + // expand HRP + for (hrp) |b| { + writer.polymodStep(@truncate(b >> 5)); + } + + writer.polymodStep(0); + for (hrp) |b| { + writer.polymodStep(@truncate(b & 0x1f)); + } + + return writer; + } + + fn polymodStep(self: *@This(), v: u5) void { + const b: u8 = @truncate(self.chk >> 25); + + self.chk = (self.chk & 0x01ff_ffff) << 5 ^ v; + + for (0.., GEN) |i, item| { + if (std.math.shr(u8, b, i) & 1 == 1) { + self.chk ^= item; + } + } + } + + pub fn finalize(self: *@This()) !void { + try self.writeChecksum(); + } + + fn writeChecksum(self: *@This()) !void { + // Pad with 6 zeros + for (0..CHECKSUM_LENGTH) |_| { + self.polymodStep(0); + } + + const plm: u32 = self.chk ^ self.variant.constant(); + + for (0..CHECKSUM_LENGTH) |p| { + const v: u8 = @intCast(std.math.shr(u32, plm, (5 * (5 - p))) & 0x1f); + + try self.formatter.writeByte(CHARSET[v]); + } + } + + /// Write a `u5` slice + fn write(self: *@This(), data: []const u5) !void { + for (data) |b| { + try self.writeU5(b); + } + } + + /// Writes a single 5 bit value of the data part + fn writeU5(self: *@This(), data: u5) !void { + self.polymodStep(data); + + try self.formatter.writeByte(CHARSET[data]); + } +}; + +// Encode a bech32 payload to string. +// +// # Errors +// * If [check_hrp] returns an error for the given HRP. +// # Deviations from standard +// * No length limits are enforced for the data part +pub fn encode(allocator: std.mem.Allocator, hrp: []const u8, data: []const u5, variant: Variant) !std.ArrayList(u8) { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + + try encodeToFmt(allocator, buf.writer().any(), hrp, data, variant); + + return buf; +} + +pub fn toBase32(allocator: std.mem.Allocator, d: []const u8) !std.ArrayList(u5) { + var self = std.ArrayList(u5).init(allocator); + errdefer self.deinit(); + + // Amount of bits left over from last round, stored in buffer. + var buffer_bits: u32 = 0; + // Holds all unwritten bits left over from last round. The bits are stored beginning from + // the most significant bit. E.g. if buffer_bits=3, then the byte with bits a, b and c will + // look as follows: [a, b, c, 0, 0, 0, 0, 0] + var buffer: u8 = 0; + + for (d) |b| { + // Write first u5 if we have to write two u5s this round. That only happens if the + // buffer holds too many bits, so we don't have to combine buffer bits with new bits + // from this rounds byte. + if (buffer_bits >= 5) { + try self.append(@truncate(std.math.shr(u8, buffer & 0b1111_1000, 3))); + buffer <<= 5; + buffer_bits -= 5; + } + + // Combine all bits from buffer with enough bits from this rounds byte so that they fill + // a u5. Save reamining bits from byte to buffer. + const from_buffer = buffer >> 3; + const from_byte = std.math.shr(u8, b, 3 + buffer_bits); // buffer_bits <= 4 + + try self.append(@truncate(from_buffer | from_byte)); + buffer = std.math.shl(u8, b, 5 - buffer_bits); + buffer_bits += 3; + } + + // There can be at most two u5s left in the buffer after processing all bytes, write them. + if (buffer_bits >= 5) { + try self.append(@truncate((buffer & 0b1111_1000) >> 3)); + buffer <<= 5; + buffer_bits -= 5; + } + + if (buffer_bits != 0) { + try self.append(@truncate(buffer >> 3)); + } + + return self; +} + +/// Encode a bech32 payload without a checksum to an [std.io.AnyWriter]. +/// +/// # Errors +/// * If [checkHrp] returns an error for the given HRP. +/// # Deviations from standard +/// * No length limits are enforced for the data part +pub fn encodeWithoutChecksumToFmt( + allocator: std.mem.Allocator, + fmt: std.io.AnyWriter, + hrp: []const u8, + data: []const u5, +) !void { + var hrp_lower = try std.ArrayList(u8).initCapacity(allocator, hrp.len); + defer hrp_lower.deinit(); + + hrp_lower.appendSliceAssumeCapacity(hrp); + + _ = if (try checkHrp(hrp) == .upper) std.ascii.lowerString(hrp_lower.items, hrp); + + _ = try fmt.write(hrp); + + _ = try fmt.writeByte(SEP); + + for (data) |b| { + try fmt.writeByte(CHARSET[b]); + } +} + +/// Encode a bech32 payload to string without the checksum. +/// +/// # Errors +/// * If [checkHrp] returns an error for the given HRP. +/// # Deviations from standard +/// * No length limits are enforced for the data part +pub fn encodeWithoutChecksum(allocator: std.mem.Allocator, hrp: []const u8, data: []const u5) !std.ArrayList(u8) { + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + + try encodeWithoutChecksumToFmt(allocator, buf.writer().any(), hrp, data); + + return buf; +} + +/// Decode a bech32 string into the raw HRP and the data bytes, assuming no checksum. +/// +/// Returns the HRP in lowercase and the data. +pub fn decodeWithoutChecksum(allocator: std.mem.Allocator, s: []const u8) Error!struct { std.ArrayList(u8), std.ArrayList(u5) } { + return splitAndDecode(allocator, s); +} + +/// Convert base32 to base256, removes null-padding if present, returns +/// `Err(Error::InvalidPadding)` if padding bits are unequal `0` +pub fn arrayListFromBase32(allocator: std.mem.Allocator, b: []const u5) !std.ArrayList(u8) { + return convertBits(u5, allocator, b, 5, 8, false); +} + +/// Convert between bit sizes +/// +/// # Errors +/// * `Error::InvalidData` if any element of `data` is out of range +/// * `Error::InvalidPadding` if `pad == false` and the padding bits are not `0` +/// +/// # Panics +/// Function will panic if attempting to convert `from` or `to` a bit size that +/// is 0 or larger than 8 bits. +/// +/// # Examples +/// +/// ```zig +/// const base5 = try convertBits(u8, allocator, &.{0xff}, 8, 5, true); +/// std.testing.expectEqualSlices(u8, base5.items, &.{0x1f, 0x1c}); +/// ``` +pub fn convertBits(comptime T: type, allocator: std.mem.Allocator, data: []const T, from: u32, to: u32, pad: bool) !std.ArrayList(u8) { + if (from > 8 or to > 8 or from == 0 or to == 0) { + @panic("convert_bits `from` and `to` parameters 0 or greater than 8"); + } + + var acc: u32 = 0; + var bits: u32 = 0; + var ret = std.ArrayList(u8).init(allocator); + errdefer ret.deinit(); + + const maxv: u32 = std.math.shl(u32, 1, to) - 1; + for (data) |value| { + const v: u32 = @intCast(value); + if (std.math.shr(u32, v, from) != 0) { + // Input value exceeds `from` bit size + return error.InvalidData; + } + acc = std.math.shl(u32, acc, from) | v; + bits += from; + + while (bits >= to) { + bits -= to; + try ret.append(@truncate(std.math.shr(u32, acc, bits) & maxv)); + } + } + + if (pad) { + if (bits > 0) { + try ret.append(@truncate(std.math.shl(u32, acc, to - bits) & maxv)); + } + } else if (bits >= from or (std.math.shl(u32, acc, to - bits) & maxv) != 0) { + return error.InvalidPadding; + } + + return ret; +} + +test "encode" { + try std.testing.expectError( + error.InvalidLength, + encode(std.testing.allocator, "", &.{ 1, 2, 3, 4 }, .bech32), + ); +} + +test "roundtrip_without_checksum" { + const hrp = "lnbc"; + const data = try toBase32(std.testing.allocator, "Hello World!"); + defer data.deinit(); + + const encoded = try encodeWithoutChecksum(std.testing.allocator, hrp, data.items); + defer encoded.deinit(); + + const decoded_hrp, const decoded_data = + try decodeWithoutChecksum(std.testing.allocator, encoded.items); + defer decoded_hrp.deinit(); + defer decoded_data.deinit(); + + try std.testing.expectEqualSlices(u8, hrp, decoded_hrp.items); + + try std.testing.expectEqualSlices(u5, data.items, decoded_data.items); +} + +test "test_hrp_case_decode" { + const hrp, const data, const variant = try decode(std.testing.allocator, "hrp1qqqq40atq3"); + defer hrp.deinit(); + defer data.deinit(); + + var expected_data = try toBase32(std.testing.allocator, &.{ 0x00, 0x00 }); + defer expected_data.deinit(); + + try std.testing.expectEqual(.bech32, variant); + try std.testing.expectEqualSlices(u8, "hrp", hrp.items); + try std.testing.expectEqualSlices(u5, expected_data.items, data.items); +} + +test "test_hrp_case" { + var data = try toBase32(std.testing.allocator, &.{ 0x00, 0x00 }); + defer data.deinit(); + + // Tests for issue with HRP case checking being ignored for encoding + const encoded = try encode(std.testing.allocator, "HRP", data.items, .bech32); + defer encoded.deinit(); + + try std.testing.expectEqualSlices(u8, "hrp1qqqq40atq3", encoded.items); +} diff --git a/src/benchmarks.zig b/src/benchmarks.zig index 1c3c704..d8a8078 100644 --- a/src/benchmarks.zig +++ b/src/benchmarks.zig @@ -1,6 +1,6 @@ const std = @import("std"); -const bdhke = @import("core/bdhke.zig"); +const dhke = @import("core/dhke.zig"); const secp256k1 = @import("core/secp256k1.zig"); const Scalar = secp256k1.Scalar; const PublicKey = secp256k1.PublicKey; @@ -16,74 +16,102 @@ pub fn main() !void { const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); - try benchmarkZul(allocator); + try benchmarkZul(); } const Context = struct { secret_msg: []const u8 = "test_message", - dhke: bdhke.Dhke, + secp: secp256k1.Secp256k1, bf: SecretKey, a: SecretKey, }; fn hashToCurve(ctx: Context, _: std.mem.Allocator, _: *std.time.Timer) !void { - _ = try bdhke.Dhke.hashToCurve(ctx.secret_msg); + _ = try dhke.hashToCurve(ctx.secret_msg); } fn step1Alice(ctx: Context, _: std.mem.Allocator, _: *std.time.Timer) !void { - _ = try ctx.dhke.step1Alice(ctx.secret_msg, ctx.bf); + const y = try dhke.hashToCurve(ctx.secret_msg); + + _ = try y.combine(secp256k1.PublicKey.fromSecretKey(ctx.secp, ctx.bf)); } fn step2Bob(ctx: Context, _: std.mem.Allocator, t: *std.time.Timer) !void { - const B_ = try ctx.dhke.step1Alice(ctx.secret_msg, ctx.bf); + const y = try dhke.hashToCurve(ctx.secret_msg); + + const B_ = try y.combine(secp256k1.PublicKey.fromSecretKey(ctx.secp, ctx.bf)); t.reset(); - _ = try ctx.dhke.step2Bob(B_, ctx.a); + _ = try B_.mulTweak(&ctx.secp, secp256k1.Scalar.fromSecretKey(ctx.a)); } fn step3Alice(ctx: Context, _: std.mem.Allocator, t: *std.time.Timer) !void { - const B_ = try ctx.dhke.step1Alice(ctx.secret_msg, ctx.bf); - const C_ = try ctx.dhke.step2Bob(B_, ctx.a); - const pub_key = ctx.a.publicKey(ctx.dhke.secp); + const y = try dhke.hashToCurve(ctx.secret_msg); + + const B_ = try y.combine(secp256k1.PublicKey.fromSecretKey(ctx.secp, ctx.bf)); + + const C_ = try B_.mulTweak(&ctx.secp, secp256k1.Scalar.fromSecretKey(ctx.a)); + + const pub_key = ctx.a.publicKey(ctx.secp); t.reset(); - _ = try ctx.dhke.step3Alice(C_, ctx.bf, pub_key); + _ = C_.combine( + (try pub_key + .mulTweak(&ctx.secp, secp256k1.Scalar.fromSecretKey(ctx.bf))) + .negate(&ctx.secp), + ) catch return error.Secp256k1Error; } fn verify(ctx: Context, _: std.mem.Allocator, t: *std.time.Timer) !void { - const B_ = try ctx.dhke.step1Alice(ctx.secret_msg, ctx.bf); - const C_ = try ctx.dhke.step2Bob(B_, ctx.a); - const pub_key = ctx.a.publicKey(ctx.dhke.secp); + const y = try dhke.hashToCurve(ctx.secret_msg); + + const B_ = try y.combine(secp256k1.PublicKey.fromSecretKey(ctx.secp, ctx.bf)); - const C = try ctx.dhke.step3Alice(C_, ctx.bf, pub_key); + const C_ = try B_.mulTweak(&ctx.secp, secp256k1.Scalar.fromSecretKey(ctx.a)); + + const pub_key = ctx.a.publicKey(ctx.secp); + + const C = C_.combine( + (try pub_key + .mulTweak(&ctx.secp, secp256k1.Scalar.fromSecretKey(ctx.bf))) + .negate(&ctx.secp), + ) catch return error.Secp256k1Error; t.reset(); - _ = try ctx.dhke.verify(ctx.a, C, ctx.secret_msg); + _ = try dhke.verifyMessage(ctx.secp, ctx.a, C, ctx.secret_msg); } fn end2End(ctx: Context, _: std.mem.Allocator, _: *std.time.Timer) !void { - const b_ = try ctx.dhke - .step1Alice(ctx.secret_msg, ctx.bf); + const y = try dhke.hashToCurve(ctx.secret_msg); + + const B_ = try y.combine(secp256k1.PublicKey.fromSecretKey(ctx.secp, ctx.bf)); - const c_ = try ctx.dhke.step2Bob(b_, ctx.a); + const C_ = try B_.mulTweak(&ctx.secp, secp256k1.Scalar.fromSecretKey(ctx.a)); - const c = try ctx.dhke - .step3Alice(c_, ctx.bf, ctx.a.publicKey(ctx.dhke.secp)); + const pub_key = ctx.a.publicKey(ctx.secp); - _ = try ctx.dhke.verify(ctx.a, c, ctx.secret_msg); + const C = C_.combine( + (try pub_key + .mulTweak(&ctx.secp, secp256k1.Scalar.fromSecretKey(ctx.bf))) + .negate(&ctx.secp), + ) catch return error.Secp256k1Error; + + _ = try dhke.verifyMessage(ctx.secp, ctx.a, C, ctx.secret_msg); } -fn benchmarkZul(allocator: std.mem.Allocator) !void { +fn benchmarkZul() !void { const a_bytes: [32]u8 = [_]u8{1} ** 32; const r_bytes: [32]u8 = [_]u8{1} ** 32; + const secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + const ctx = Context{ - .dhke = try bdhke.Dhke.init(allocator), + .secp = secp, .a = try SecretKey.fromSlice(&a_bytes), .bf = try SecretKey.fromSlice(&r_bytes), }; - defer ctx.dhke.deinit(); (try zul.benchmark.runC(ctx, hashToCurve, .{})).print("hashToCurve"); (try zul.benchmark.runC(ctx, step1Alice, .{})).print("step1Alice"); diff --git a/src/core/amount.zig b/src/core/amount.zig new file mode 100644 index 0000000..69d607e --- /dev/null +++ b/src/core/amount.zig @@ -0,0 +1,194 @@ +const std = @import("std"); + +pub const Amount = u64; + +pub fn split(self: Amount, allocator: std.mem.Allocator) !std.ArrayList(Amount) { + const sats: u64 = self; + var result = std.ArrayList(Amount).init(allocator); + errdefer result.deinit(); + + var i: u64 = 64; + while (i > 0) { + i -= 1; + + const part = std.math.shl(u64, 1, i); + + if ((sats & part) == part) { + try result.append(part); + } + } + + return result; +} + +pub fn sum(t: []const Amount) Amount { + var a: Amount = 0; + for (t) |e| a += e; + return a; +} + +/// Split into parts that are powers of two by target +pub fn splitTargeted(self: Amount, allocator: std.mem.Allocator, target: SplitTarget) !std.ArrayList(Amount) { + const parts = switch (target) { + .none => try split(self, allocator), + .value => |amount| v: { + if (self <= amount) { + return split(self, allocator); + } + + var parts_total: Amount = 0; + var parts = std.ArrayList(Amount).init(allocator); + errdefer parts.deinit(); + + // The powers of two that are need to create target value + const parts_of_value = try split(amount, allocator); + defer parts_of_value.deinit(); + + while (parts_total < self) { + for (parts_of_value.items) |part| { + if ((part + parts_total) <= self) { + try parts.append(part); + } else { + const amount_left = self - parts_total; + const amount_left_splitted = try split(amount_left, allocator); + defer amount_left_splitted.deinit(); + + try parts.appendSlice(amount_left_splitted.items); + } + + parts_total = sum(parts.items); + + if (parts_total == self) { + break; + } + } + } + + break :v parts; + }, + .values => |values| v: { + const values_total: Amount = sum(values); + + switch (std.math.order(self, values_total)) { + .eq => { + var result = try std.ArrayList(Amount).initCapacity(allocator, values.len); + errdefer result.deinit(); + + result.appendSliceAssumeCapacity(values); + break :v result; + }, + .lt => return error.SplitValuesGreater, + .gt => { + const extra = self - values_total; + var extra_amount = try split(extra, allocator); + defer extra_amount.deinit(); + + var result = try std.ArrayList(Amount).initCapacity(allocator, values.len + extra_amount.items.len); + errdefer result.deinit(); + + result.appendSliceAssumeCapacity(values); + result.appendSliceAssumeCapacity(extra_amount.items); + + break :v result; + }, + } + }, + }; + + std.sort.block(Amount, parts.items, {}, (struct { + pub fn compare(_: void, lhs: Amount, rhs: Amount) bool { + return lhs < rhs; + } + }).compare); + + return parts; +} + +/// Kinds of targeting that are supported +pub const SplitTarget = union(enum) { + /// Default target; least amount of proofs + none, + /// Target amount for wallet to have most proofs that add up to value + value: Amount, + /// Specific amounts to split into **MUST** equal amount being split + values: []const Amount, +}; + +test "test_split_amount" { + { + var splitted = try split(@as(Amount, 1), std.testing.allocator); + + defer splitted.deinit(); + try std.testing.expectEqualSlices(Amount, &.{1}, splitted.items); + } + { + var splitted = try split(@as(Amount, 2), std.testing.allocator); + + defer splitted.deinit(); + try std.testing.expectEqualSlices(Amount, &.{2}, splitted.items); + } + { + var splitted = try split(3, std.testing.allocator); + + defer splitted.deinit(); + try std.testing.expectEqualSlices(Amount, &.{ 2, 1 }, splitted.items); + } + { + var splitted = try split(11, std.testing.allocator); + + defer splitted.deinit(); + try std.testing.expectEqualSlices(Amount, &.{ 8, 2, 1 }, splitted.items); + } + { + var splitted = try split(255, std.testing.allocator); + + defer splitted.deinit(); + try std.testing.expectEqualSlices(Amount, &.{ 128, 64, 32, 16, 8, 4, 2, 1 }, splitted.items); + } +} + +test "test_split_target_amount" { + { + const splitted = + try splitTargeted(65, std.testing.allocator, .{ .value = 32 }); + defer splitted.deinit(); + + try std.testing.expectEqualSlices(Amount, &.{ 1, 32, 32 }, splitted.items); + } + { + const splitted = + try splitTargeted(150, std.testing.allocator, .{ .value = 50 }); + defer splitted.deinit(); + + try std.testing.expectEqualSlices(Amount, &.{ 2, 2, 2, 16, 16, 16, 32, 32, 32 }, splitted.items); + } + + { + const splitted = + try splitTargeted(63, std.testing.allocator, .{ .value = 32 }); + defer splitted.deinit(); + + try std.testing.expectEqualSlices(Amount, &.{ 1, 2, 4, 8, 16, 32 }, splitted.items); + } +} + +test "test_split_values" { + { + const target: []const Amount = &.{ 2, 4, 4 }; + + const values = try splitTargeted(10, std.testing.allocator, .{ .values = target }); + defer values.deinit(); + + try std.testing.expectEqualSlices(Amount, target, values.items); + } + { + const target: []const Amount = &.{ 2, 4, 4 }; + + const values = try splitTargeted(10, std.testing.allocator, .{ .values = &.{ 2, 4 } }); + defer values.deinit(); + + try std.testing.expectEqualSlices(Amount, target, values.items); + } + + try std.testing.expectError(error.SplitValuesGreater, splitTargeted(10, std.testing.allocator, .{ .values = &.{ 2, 10 } })); +} diff --git a/src/core/bdhke.zig b/src/core/bdhke.zig deleted file mode 100644 index 709d4e1..0000000 --- a/src/core/bdhke.zig +++ /dev/null @@ -1,252 +0,0 @@ -const std = @import("std"); -const secp256k1 = @import("secp256k1.zig"); - -const crypto = std.crypto; - -pub const Dhke = struct { - const Self = @This(); - secp: secp256k1.Secp256k1, - allocator: std.mem.Allocator, - - pub fn init(allocator: std.mem.Allocator) !Self { - return .{ - .secp = try secp256k1.Secp256k1.genNew(allocator), - .allocator = allocator, - }; - } - - pub fn deinit(self: Self) void { - self.secp.deinit(self.allocator); - } - - pub fn hashToCurve(message: []const u8) !secp256k1.PublicKey { - const domain_separator = "Secp256k1_HashToCurve_Cashu_"; - var hasher = crypto.hash.sha2.Sha256.init(.{}); - - hasher.update(domain_separator); - hasher.update(message); - - const msg_to_hash = hasher.finalResult(); - - var buf: [33]u8 = undefined; - buf[0] = 0x02; - - var counter_buf: [4]u8 = undefined; - - const till = comptime try std.math.powi(u32, 2, 16); - - var counter: u32 = 0; - - while (counter < till) : (counter += 1) { - hasher = crypto.hash.sha2.Sha256.init(.{}); - - hasher.update(&msg_to_hash); - std.mem.writeInt(u32, &counter_buf, counter, .little); - hasher.update(&counter_buf); - hasher.final(buf[1..]); - - const pk = secp256k1.PublicKey.fromSlice(&buf) catch continue; - - return pk; - } - - return error.NoValidPointFound; - } - - pub fn step1Alice(self: Self, sec_msg: []const u8, blinding_factor: secp256k1.SecretKey) !secp256k1.PublicKey { - const y = try Self.hashToCurve(sec_msg); - - const b = try y.combine(secp256k1.PublicKey.fromSecretKey(self.secp, blinding_factor)); - return b; - } - - pub fn step2Bob(self: Self, b: secp256k1.PublicKey, a: secp256k1.SecretKey) !secp256k1.PublicKey { - return try b.mulTweak(&self.secp, secp256k1.Scalar.fromSecretKey(a)); - } - - pub fn step3Alice(self: Self, c_: secp256k1.PublicKey, r: secp256k1.SecretKey, a: secp256k1.PublicKey) !secp256k1.PublicKey { - return c_.combine( - (try a - .mulTweak(&self.secp, secp256k1.Scalar.fromSecretKey(r))) - .negate(&self.secp), - ) catch return error.Secp256k1Error; - } - - pub fn verify(self: Self, a: secp256k1.SecretKey, c: secp256k1.PublicKey, secret_msg: []const u8) !bool { - const y = try Self.hashToCurve(secret_msg); - - const res = try y.mulTweak(&self.secp, secp256k1.Scalar.fromSecretKey(a)); - - return std.meta.eql(c.pk, res.pk); - } -}; - -/// End-to-end test scenario for BDHKE -pub fn testBDHKE(allocator: std.mem.Allocator) !void { - // Initialize with deterministic values - const secret_msg = "test_message"; - var a_bytes: [32]u8 = [_]u8{0} ** 31 ++ [_]u8{1}; - var r_bytes: [32]u8 = [_]u8{0} ** 31 ++ [_]u8{1}; - - const dhke = try Dhke.init(allocator); - defer dhke.deinit(); - - const a = try secp256k1.SecretKey.fromSlice(&a_bytes); - const bf = try secp256k1.SecretKey.fromSlice(&r_bytes); - - std.debug.print("Starting BDHKE test\n", .{}); - std.debug.print("Secret message: {s}\n", .{secret_msg}); - std.debug.print("Alice's private key (a): {s}\n", .{std.fmt.fmtSliceHexLower(&a_bytes)}); - std.debug.print("Alice's public key (A): {s}\n", .{std.fmt.fmtSliceHexLower(&a.publicKey(dhke.secp).serialize())}); - - // Deterministic blinding factor - std.debug.print("r private key: {s}\n", .{std.fmt.fmtSliceHexLower(&r_bytes)}); - std.debug.print("Blinding factor (r): {s}\n", .{std.fmt.fmtSliceHexLower(&bf.publicKey(dhke.secp).serialize())}); - - // Step 1: Alice blinds the message - const B_ = try dhke.step1Alice(secret_msg, bf); - std.debug.print("Blinded message (B_): {s}\n", .{std.fmt.fmtSliceHexLower(&B_.serialize())}); - std.debug.print("Step 1 complete: Message blinded\n", .{}); - - // Step 2: Bob signs the blinded message - const C_ = try dhke.step2Bob(B_, a); - - std.debug.print("Blinded signature (C_): {s}\n", .{std.fmt.fmtSliceHexLower(&C_.serialize())}); - std.debug.print("Step 2 complete: Blinded message signed\n", .{}); - - // Step 3: Alice unblinds the signature - const C = try dhke.step3Alice(C_, bf, a.publicKey(dhke.secp)); - std.debug.print("Unblinded signature (C): {s}\n", .{std.fmt.fmtSliceHexLower(&C.serialize())}); - std.debug.print("Step 3 complete: Signature unblinded\n", .{}); - - // Final verification - const final_verification = try dhke.verify(a, C, secret_msg); - if (!final_verification) { - return error.VerificationFailed; - } - std.debug.print("Final verification successful\n", .{}); - - std.debug.print("BDHKE test completed successfully\n", .{}); -} - -test "testBdhke" { - const secret_msg = "test_message"; - const a = try secp256k1.SecretKey.fromSlice(&[_]u8{1} ** 32); - const blinding_factor = try secp256k1.SecretKey.fromSlice(&[_]u8{1} ** 32); - - const dhke = try Dhke.init(std.testing.allocator); - defer dhke.deinit(); - - const _b = try dhke.step1Alice(secret_msg, blinding_factor); - - const _c = try dhke.step2Bob(_b, a); - - const step3_c = try dhke.step3Alice(_c, blinding_factor, a.publicKey(dhke.secp)); - - const res = try dhke.verify(a, step3_c, secret_msg); - - try std.testing.expect(res); -} - -test "test_hash_to_curve_zero" { - var buffer: [64]u8 = undefined; - const hex = "0000000000000000000000000000000000000000000000000000000000000000"; - const expected_result = "024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725"; - - const res = try Dhke.hashToCurve(try std.fmt.hexToBytes(&buffer, hex)); - - try std.testing.expectEqualSlices(u8, try std.fmt.hexToBytes(&buffer, expected_result), &res.serialize()); -} - -test "test_hash_to_curve_one" { - var buffer: [64]u8 = undefined; - const hex = "0000000000000000000000000000000000000000000000000000000000000001"; - const expected_result = "022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf"; - - const res = try Dhke.hashToCurve(try std.fmt.hexToBytes(&buffer, hex)); - - try std.testing.expectEqualSlices(u8, try std.fmt.hexToBytes(&buffer, expected_result), &res.serialize()); -} - -test "test_hash_to_curve_two" { - var buffer: [64]u8 = undefined; - const hex = "0000000000000000000000000000000000000000000000000000000000000002"; - const expected_result = "026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f"; - - const res = try Dhke.hashToCurve(try std.fmt.hexToBytes(&buffer, hex)); - - try std.testing.expectEqualSlices(u8, try std.fmt.hexToBytes(&buffer, expected_result), &res.serialize()); -} - -test "test_step1_alice" { - const dhke = try Dhke.init(std.testing.allocator); - defer dhke.deinit(); - - var hex_buffer: [64]u8 = undefined; - - const bf = try secp256k1.SecretKey.fromSlice(try std.fmt.hexToBytes(&hex_buffer, "0000000000000000000000000000000000000000000000000000000000000001")); - - const pub_key = try dhke.step1Alice("test_message", bf); - - try std.testing.expectEqualSlices(u8, try std.fmt.hexToBytes(&hex_buffer, "025cc16fe33b953e2ace39653efb3e7a7049711ae1d8a2f7a9108753f1cdea742b"), &pub_key.serialize()); -} - -test "test_step2_bob" { - const dhke = try Dhke.init(std.testing.allocator); - defer dhke.deinit(); - - var hex_buffer: [64]u8 = undefined; - - const bf = try secp256k1.SecretKey.fromSlice(try std.fmt.hexToBytes(&hex_buffer, "0000000000000000000000000000000000000000000000000000000000000001")); - - const pub_key = try dhke.step1Alice("test_message", bf); - - const a = try secp256k1.SecretKey.fromSlice(try std.fmt.hexToBytes(&hex_buffer, "0000000000000000000000000000000000000000000000000000000000000001")); - - const c = try dhke.step2Bob(pub_key, a); - - try std.testing.expectEqualSlices(u8, try std.fmt.hexToBytes(&hex_buffer, "025cc16fe33b953e2ace39653efb3e7a7049711ae1d8a2f7a9108753f1cdea742b"), &c.serialize()); -} - -test "test_step3_alice" { - const dhke = try Dhke.init(std.testing.allocator); - defer dhke.deinit(); - - var hex_buffer: [64]u8 = undefined; - - const c_ = try secp256k1.PublicKey.fromSlice(try std.fmt.hexToBytes(&hex_buffer, "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2")); - - const bf = try secp256k1.SecretKey.fromSlice(try std.fmt.hexToBytes(&hex_buffer, "0000000000000000000000000000000000000000000000000000000000000001")); - - const a = try secp256k1.PublicKey.fromSlice(try std.fmt.hexToBytes(&hex_buffer, "020000000000000000000000000000000000000000000000000000000000000001")); - - const result = try dhke.step3Alice(c_, bf, a); - - try std.testing.expectEqualSlices(u8, try std.fmt.hexToBytes(&hex_buffer, "03c724d7e6a5443b39ac8acf11f40420adc4f99a02e7cc1b57703d9391f6d129cd"), &result.serialize()); -} - -test "test_verify" { - const dhke = try Dhke.init(std.testing.allocator); - defer dhke.deinit(); - - var hex_buffer: [64]u8 = undefined; - - // Generate Alice's private key and public key - const a = try secp256k1.SecretKey.fromSlice(try std.fmt.hexToBytes(&hex_buffer, "0000000000000000000000000000000000000000000000000000000000000001")); - - const A = a.publicKey(dhke.secp); - - const bf = try secp256k1.SecretKey.fromSlice(try std.fmt.hexToBytes(&hex_buffer, "0000000000000000000000000000000000000000000000000000000000000002")); - - // Generate a shared secret - - const secret_msg = "test"; - - const B_ = try dhke.step1Alice(secret_msg, bf); - const C_ = try dhke.step2Bob(B_, a); - const C = try dhke.step3Alice(C_, bf, A); - - try std.testing.expect(try dhke.verify(a, C, secret_msg)); - try std.testing.expect(!try dhke.verify(a, try C.combine(C), secret_msg)); - try std.testing.expect(!try dhke.verify(a, A, secret_msg)); -} diff --git a/src/core/bip32/bip32.zig b/src/core/bip32/bip32.zig new file mode 100644 index 0000000..ff15d07 --- /dev/null +++ b/src/core/bip32/bip32.zig @@ -0,0 +1,774 @@ +//! BIP32 implementation. +//! +//! Implementation of BIP32 hierarchical deterministic wallets, as defined +//! at . +//! +const Ripemd160 = @import("../../mint/lightning/invoices/ripemd160.zig").Ripemd160; +const secp256k1 = @import("../secp256k1.zig"); +const Secp256k1NumberOfPoints = 115792089237316195423570985008687907852837564279074904382605163141518161494337; +const key_lib = @import("key.zig"); + +const base58 = @import("base58"); + +const std = @import("std"); +const Hmac = std.crypto.auth.hmac.sha2.HmacSha512; + +pub const Network = enum { MAINNET, TESTNET, REGTEST, SIMNET }; + +pub const SerializedPrivateKeyVersion = enum(u32) { + MAINNET = 0x0488aDe4, + TESTNET = 0x04358394, + SEGWIT_MAINNET = 0x04b2430c, + SEGWIT_TESTNET = 0x045f18bc, +}; + +pub const SerializedPublicKeyVersion = enum(u32) { + MAINNET = 0x0488b21e, + TESTNET = 0x043587cf, + SEGWIT_MAINNET = 0x04b24746, + SEGWIT_TESTNET = 0x045f1cf6, +}; + +/// A chain code +pub const ChainCode = struct { + inner: [32]u8, + + fn fromHmac(hmac: [64]u8) ChainCode { + return .{ .inner = hmac[32..].* }; + } +}; + +/// A fingerprint +pub const Fingerprint = struct { + inner: [4]u8, +}; + +fn base58EncodeCheck(allocator: std.mem.Allocator, data: []const u8) ![]u8 { + const encoder = base58.Encoder.init(.{}); + + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + hasher.update(data); + var checksum = hasher.finalResult(); + + hasher = std.crypto.hash.sha2.Sha256.init(.{}); + hasher.update(&checksum); + checksum = hasher.finalResult(); + + var encoding_data = try allocator.alloc(u8, data.len + 4); + defer allocator.free(encoding_data); + + @memcpy(encoding_data[0..data.len], data); + @memcpy(encoding_data[data.len..], checksum[0..4]); + + return try encoder.encodeAlloc(allocator, encoding_data); +} + +fn base58DecodeCheck(allocator: std.mem.Allocator, data: []const u8) ![]u8 { + const decoder = base58.Decoder.init(.{}); + + const decoded = try decoder.decodeAlloc(allocator, data); + defer allocator.free(decoded); + if (decoded.len < 4) return error.TooShortError; + + const check_start = decoded.len - 4; + + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + + hasher.update(decoded[0..check_start]); + const fr = hasher.finalResult(); + + hasher = std.crypto.hash.sha2.Sha256.init(.{}); + hasher.update(&fr); + + const hash_check = hasher.finalResult()[0..4].*; + const data_check = decoded[check_start..][0..4].*; + + const expected = std.mem.readInt(u32, &hash_check, .little); + const actual = std.mem.readInt(u32, &data_check, .little); + + if (expected != actual) return error.IncorrectChecksum; + + const result = try allocator.alloc(u8, check_start); + errdefer allocator.free(result); + + @memcpy(result, decoded[0..check_start]); + return result; +} + +/// Extended private key +pub const ExtendedPrivKey = struct { + /// The network this key is to be used on + network: Network, + /// How many derivations this key is from the master (which is 0) + depth: u8, + /// Fingerprint of the parent key (0 for master) + parent_fingerprint: Fingerprint, + /// Child number of the key used to derive from parent (0 for master) + child_number: ChildNumber, + /// Private key + private_key: secp256k1.SecretKey, + /// Chain code + chain_code: ChainCode, + + pub fn fromStr(allocator: std.mem.Allocator, s: []const u8) !ExtendedPrivKey { + const decoded = try base58DecodeCheck(allocator, s); + defer allocator.free(decoded); + + if (decoded.len != 78) return error.InvalidLength; + + return try decode(decoded); + } + + pub fn toStr(self: ExtendedPrivKey, allocator: std.mem.Allocator) ![]const u8 { + const encoded = self.encode(); + return base58EncodeCheck(allocator, &encoded); + } + + /// Extended private key binary encoding according to BIP 32 + pub fn encode(self: ExtendedPrivKey) [78]u8 { + var ret = [_]u8{0} ** 78; + + ret[0..4].* = switch (self.network) { + .MAINNET => .{ 0x04, 0x88, 0xAD, 0xE4 }, + else => .{ 0x04, 0x35, 0x83, 0x94 }, + }; + + ret[4] = self.depth; + ret[5..9].* = self.parent_fingerprint.inner; + + var buf: [4]u8 = undefined; + std.mem.writeInt(u32, &buf, self.child_number.toU32(), .big); + + ret[9..13].* = buf; + ret[13..45].* = self.chain_code.inner; + ret[45] = 0; + ret[46..78].* = self.private_key.data; + return ret; + } + + /// Construct a new master key from a seed value + pub fn initMaster(network: Network, seed: []const u8) !ExtendedPrivKey { + var hmac_engine = Hmac.init("Bitcoin seed"); + hmac_engine.update(seed); + var hmac_result: [Hmac.mac_length]u8 = undefined; + + hmac_engine.final(&hmac_result); + + return ExtendedPrivKey{ + .network = network, + .depth = 0, + .parent_fingerprint = .{ .inner = .{ 0, 0, 0, 0 } }, + .child_number = try ChildNumber.fromNormalIdx(0), + .private_key = try secp256k1.SecretKey.fromSlice(hmac_result[0..32]), + .chain_code = ChainCode.fromHmac(hmac_result), + }; + } + + /// Constructs ECDSA compressed private key matching internal secret key representation. + pub fn toPrivateKey(self: ExtendedPrivKey) key_lib.PrivateKey { + return .{ + .compressed = true, + .network = self.network, + .inner = self.private_key, + }; + } + + /// Constructs BIP340 keypair for Schnorr signatures and Taproot use matching the internal + /// secret key representation. + pub fn toKeypair(self: ExtendedPrivKey, secp: secp256k1.Secp256k1) secp256k1.KeyPair { + return secp256k1.KeyPair.fromSecretKey(&secp, &self.private_key) catch @panic("BIP32 internal private key representation is broken"); + } + + /// Private->Private child key derivation + pub fn ckdPriv( + self: ExtendedPrivKey, + secp: secp256k1.Secp256k1, + i: ChildNumber, + ) !ExtendedPrivKey { + var hmac_engine = Hmac.init(self.chain_code.inner[0..]); + switch (i) { + .normal => { + // Non-hardened key: compute public data and use that + hmac_engine.update(&self.private_key.publicKey(secp).serialize()); + }, + .hardened => { + // Hardened key: use only secret data to prevent public derivation + hmac_engine.update(&.{0}); + hmac_engine.update(self.private_key.data[0..]); + }, + } + + const i_u32 = i.toU32(); + var buf: [4]u8 = undefined; + + std.mem.writeInt(u32, &buf, i_u32, .big); + + hmac_engine.update(&buf); + + var hmac_result: [Hmac.mac_length]u8 = undefined; + + hmac_engine.final(&hmac_result); + + const sk = secp256k1.SecretKey.fromSlice(hmac_result[0..32]) catch @panic("statistically impossible to hit"); + const tweaked = sk.addTweak(secp256k1.Scalar.fromSecretKey(self.private_key)) catch @panic("statistically impossible to hit"); + + return .{ + .network = self.network, + .depth = self.depth + 1, + .parent_fingerprint = self.fingerprint(secp), + .child_number = i, + .private_key = tweaked, + .chain_code = ChainCode.fromHmac(hmac_result), + }; + } + + /// Attempts to derive an extended private key from a path. + /// + /// The `path` argument can be both of type `DerivationPath` or `Vec`. + pub fn derivePriv( + self: ExtendedPrivKey, + secp: secp256k1.Secp256k1, + path: []const ChildNumber, + ) !ExtendedPrivKey { + var sk = self; + for (path) |cnum| { + sk = try sk.ckdPriv(secp, cnum); + } + + return sk; + } + + /// Returns the HASH160 of the public key belonging to the xpriv + pub fn identifier(self: ExtendedPrivKey, secp: secp256k1.Secp256k1) XpubIdentifier { + return ExtendedPubKey.fromPrivateKey(secp, self).identifier(); + } + + /// Returns the first four bytes of the identifier + pub fn fingerprint(self: ExtendedPrivKey, secp: secp256k1.Secp256k1) Fingerprint { + return .{ .inner = self.identifier(secp).inner[0..4].* }; + } + + /// Decoding extended private key from binary data according to BIP 32 + pub fn decode(data: []const u8) !ExtendedPrivKey { + if (data.len != 78) { + return error.WrongExtendedKeyLength; + } + + const network = if (std.mem.eql(u8, data[0..4], &.{ 0x04, 0x88, 0xAD, 0xE4 })) + Network.MAINNET + else if (std.mem.eql(u8, data[0..4], &.{ 0x04, 0x35, 0x83, 0x94 })) + Network.TESTNET + else + return error.UnknownVersion; + + return .{ + .network = network, + .depth = data[4], + .parent_fingerprint = .{ .inner = data[5..9].* }, + .child_number = ChildNumber.fromU32(std.mem.readInt(u32, data[9..13], .big)), + .chain_code = .{ .inner = data[13..45].* }, + .private_key = try secp256k1.SecretKey.fromSlice(data[46..78]), + }; + } +}; + +/// Extended public key +pub const ExtendedPubKey = struct { + /// The network this key is to be used on + network: Network, + /// How many derivations this key is from the master (which is 0) + depth: u8, + /// Fingerprint of the parent key + parent_fingerprint: Fingerprint, + /// Child number of the key used to derive from parent (0 for master) + child_number: ChildNumber, + /// Public key + public_key: secp256k1.PublicKey, + /// Chain code + chain_code: ChainCode, + + pub fn fromStr(allocator: std.mem.Allocator, s: []const u8) !ExtendedPubKey { + const decoded = try base58DecodeCheck(allocator, s); + defer allocator.free(decoded); + + if (decoded.len != 78) return error.InvalidLength; + + return try decode(decoded); + } + + pub fn toStr(self: ExtendedPubKey, allocator: std.mem.Allocator) ![]const u8 { + return try base58EncodeCheck(allocator, &self.encode()); + } + + /// Extended public key binary encoding according to BIP 32 + pub fn encode(self: ExtendedPubKey) [78]u8 { + var ret = [_]u8{0} ** 78; + + ret[0..4].* = switch (self.network) { + .MAINNET => .{ 0x04, 0x88, 0xB2, 0x1E }, + else => .{ 0x04, 0x35, 0x87, 0xCF }, + }; + + ret[4] = self.depth; + ret[5..9].* = self.parent_fingerprint.inner; + + var buf: [4]u8 = undefined; + std.mem.writeInt(u32, &buf, self.child_number.toU32(), .big); + + ret[9..13].* = buf; + ret[13..45].* = self.chain_code.inner; + ret[45..78].* = self.public_key.serialize(); + return ret; + } + + pub fn decode(data: []const u8) !ExtendedPubKey { + if (data.len != 78) { + return error.WrongExtendedKeyLength; + } + + const network = if (std.mem.eql(u8, data[0..4], &.{ 0x04, 0x88, 0xB2, 0x1E })) + Network.MAINNET + else if (std.mem.eql(u8, data[0..4], &.{ 0x04, 0x35, 0x87, 0xCF })) + Network.TESTNET + else + return error.UnknownVersion; + + return .{ + .network = network, + .depth = data[4], + .parent_fingerprint = .{ .inner = data[5..9].* }, + .child_number = ChildNumber.fromU32(std.mem.readInt(u32, data[9..13], .big)), + .chain_code = .{ .inner = data[13..45].* }, + .public_key = try secp256k1.PublicKey.fromSlice(data[45..78]), + }; + } + + /// Derives a public key from a private key + pub fn fromPrivateKey( + secp: secp256k1.Secp256k1, + sk: ExtendedPrivKey, + ) ExtendedPubKey { + return .{ + .network = sk.network, + .depth = sk.depth, + .parent_fingerprint = sk.parent_fingerprint, + .child_number = sk.child_number, + .public_key = sk.private_key.publicKey(secp), + .chain_code = sk.chain_code, + }; + } + + /// Attempts to derive an extended public key from a path. + /// + /// The `path` argument can be any type implementing `AsRef`, such as `DerivationPath`, for instance. + pub fn derivePub( + self: ExtendedPubKey, + secp: secp256k1.Secp256k1, + path: []ChildNumber, + ) !ExtendedPubKey { + var pk = self; + for (path) |cnum| { + pk = try pk.ckdPub(secp, cnum); + } + + return pk; + } + + /// Compute the scalar tweak added to this key to get a child key + pub fn ckdPubTweak( + self: ExtendedPubKey, + i: ChildNumber, + ) !struct { secp256k1.SecretKey, ChainCode } { + switch (i) { + .hardened => return error.CannotDeriveFromHardenedKey, + .normal => |n| { + var hmac_engine = Hmac.init(&self.chain_code.inner); + + hmac_engine.update(&self.public_key.serialize()); + + var buf: [4]u8 = undefined; + std.mem.writeInt(u32, &buf, n, .big); + + hmac_engine.update(&buf); + var hmac_result: [Hmac.mac_length]u8 = undefined; + hmac_engine.final(&hmac_result); + + const private_key = try secp256k1.SecretKey.fromSlice(hmac_result[0..32]); + const chain_code = ChainCode.fromHmac(hmac_result); + + return .{ private_key, chain_code }; + }, + } + } + + /// Public->Public child key derivation + pub fn ckdPub( + self: ExtendedPubKey, + secp: secp256k1.Secp256k1, + i: ChildNumber, + ) !ExtendedPubKey { + const sk, const chain_code = try self.ckdPubTweak(i); + + const tweaked = try self.public_key.addExpTweak(secp, secp256k1.Scalar.fromSecretKey(sk)); + + return .{ + .network = self.network, + .depth = self.depth + 1, + .parent_fingerprint = self.fingerprint(), + .child_number = i, + .public_key = tweaked, + .chain_code = chain_code, + }; + } + + /// Returns the HASH160 of the chaincode + pub fn identifier(self: ExtendedPubKey) XpubIdentifier { + return .{ .inner = hash160(&self.public_key.serialize()) }; + } + + /// Returns the first four bytes of the identifier + pub fn fingerprint(self: ExtendedPubKey) Fingerprint { + return .{ .inner = self.identifier().inner[0..4].* }; + } +}; + +fn hash160(data: []const u8) [Ripemd160.digest_length]u8 { + var hasher256 = std.crypto.hash.sha2.Sha256.init(.{}); + hasher256.update(data); + + var out256: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; + hasher256.final(&out256); + + var hasher = Ripemd160.init(.{}); + hasher.update(&out256); + + var out: [Ripemd160.digest_length]u8 = undefined; + hasher.final(&out); + return out; +} + +pub const XpubIdentifier = struct { + inner: [Ripemd160.digest_length]u8, +}; + +/// A child number for a derived key +pub const ChildNumber = union(enum) { + /// Non-hardened key + /// Key index, within [0, 2^31 - 1] + normal: u32, + /// Hardened key + /// Key index, within [0, 2^31 - 1] + hardened: u32, + + pub fn fromStr(inp: []const u8) !ChildNumber { + const is_hardened = (inp[inp.len - 1] == '\'' or inp[inp.len - 1] == 'h'); + + if (is_hardened) return try fromHardenedIdx(try std.fmt.parseInt(u32, inp[0 .. inp.len - 1], 10)) else return try fromNormalIdx(try std.fmt.parseInt(u32, inp, 10)); + } + + /// Create a [`Normal`] from an index, returns an error if the index is not within + /// [0, 2^31 - 1]. + /// + /// [`Normal`]: #variant.Normal + pub fn fromNormalIdx(index: u32) !ChildNumber { + if ((index & (1 << 31)) == 0) + return .{ .normal = index }; + + return error.InvalidChildNumber; + } + + /// Create a [`Hardened`] from an index, returns an error if the index is not within + /// [0, 2^31 - 1]. + /// + /// [`Hardened`]: #variant.Hardened + pub fn fromHardenedIdx(index: u32) !ChildNumber { + if (index & (1 << 31) == 0) + return .{ .hardened = index }; + + return error.InvalidChildNumber; + } + + /// Returns `true` if the child number is a [`Normal`] value. + /// + /// [`Normal`]: #variant.Normal + pub fn isNormal(self: ChildNumber) bool { + return !self.isHardened(); + } + + /// Returns `true` if the child number is a [`Hardened`] value. + /// + /// [`Hardened`]: #variant.Hardened + pub fn isHardened(self: ChildNumber) bool { + return switch (self) { + .hardened => true, + .normal => false, + }; + } + /// Returns the child number that is a single increment from this one. + pub fn increment(self: ChildNumber) !ChildNumber { + return switch (self) { + .hardened => |idx| try fromHardenedIdx(idx + 1), + .normal => |idx| try fromNormalIdx(idx + 1), + }; + } + + fn fromU32(number: u32) ChildNumber { + if (number & (1 << 31) != 0) { + return .{ + .hardened = number ^ (1 << 31), + }; + } else { + return .{ .normal = number }; + } + } + + fn toU32(self: ChildNumber) u32 { + return switch (self) { + .normal => |index| index, + .hardened => |index| index | (1 << 31), + }; + } +}; + +fn testPath( + secp: secp256k1.Secp256k1, + network: Network, + seed: []const u8, + path: []ChildNumber, + expected_sk: []const u8, + expected_pk: []const u8, +) !void { + var sk = try ExtendedPrivKey.initMaster(network, seed); + var pk = ExtendedPubKey.fromPrivateKey(secp, sk); + + // Check derivation convenience method for ExtendedPrivKey + { + const actual_sk = try (try sk.derivePriv(secp, path)).toStr(std.testing.allocator); + defer std.testing.allocator.free(actual_sk); + + try std.testing.expectEqualSlices( + u8, + actual_sk, + expected_sk, + ); + } + + // Check derivation convenience method for ExtendedPubKey, should error + // appropriately if any ChildNumber is hardened + for (path) |cnum| { + if (cnum.isHardened()) { + try std.testing.expectError(error.CannotDeriveFromHardenedKey, pk.derivePub(secp, path)); + break; + } + } else { + const derivedPub = try (try pk.derivePub(secp, path)).toStr(std.testing.allocator); + defer std.testing.allocator.free(derivedPub); + + try std.testing.expectEqualSlices(u8, derivedPub, expected_pk); + } + + // Derive keys, checking hardened and non-hardened derivation one-by-one + for (path) |num| { + sk = try sk.ckdPriv(secp, num); + switch (num) { + .normal => { + const pk2 = try pk.ckdPub(secp, num); + pk = ExtendedPubKey.fromPrivateKey(secp, sk); + try std.testing.expectEqualDeep(pk, pk2); + }, + .hardened => { + try std.testing.expectError(error.CannotDeriveFromHardenedKey, pk.ckdPub(secp, num)); + pk = ExtendedPubKey.fromPrivateKey(secp, sk); + }, + } + } + // Check result against expected base58 + const skStr = try sk.toStr(std.testing.allocator); + defer std.testing.allocator.free(skStr); + try std.testing.expectEqualSlices(u8, skStr, expected_sk); + + const pkStr = try pk.toStr(std.testing.allocator); + defer std.testing.allocator.free(pkStr); + try std.testing.expectEqualSlices(u8, pkStr, expected_pk); + + // Check decoded base58 against result + const decoded_sk = try ExtendedPrivKey.fromStr(std.testing.allocator, expected_sk); + const decoded_pk = try ExtendedPubKey.fromStr(std.testing.allocator, expected_pk); + + try std.testing.expectEqualDeep(decoded_sk, sk); + try std.testing.expectEqualDeep(decoded_pk, pk); +} + +fn derivatePathFromStr(path: []const u8, allocator: std.mem.Allocator) !std.ArrayList(ChildNumber) { + if (path.len == 0 or (path.len == 1 and path[0] == 'm') or (path.len == 2 and path[0] == 'm' and path[1] == '/')) return std.ArrayList(ChildNumber).init(allocator); + + var p = path; + + if (std.mem.startsWith(u8, path, "m/")) p = path[2..]; + + var parts = std.mem.splitScalar(u8, p, '/'); + + var result = std.ArrayList(ChildNumber).init(allocator); + errdefer result.deinit(); + + while (parts.next()) |s| { + try result.append(try ChildNumber.fromStr(s)); + } + + return result; +} + +test "schnorr_broken_privkey_ffs" { + // Xpriv having secret key set to all 0xFF's + const xpriv_str = "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fENZ3QzxW"; + try std.testing.expectError(error.InvalidSecretKey, ExtendedPrivKey.fromStr(std.testing.allocator, xpriv_str)); +} + +test "vector_1" { + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + var buf: [100]u8 = undefined; + + const seed = try std.fmt.hexToBytes(&buf, "000102030405060708090a0b0c0d0e0f"); + // derivation path, expected_sk , expected_pk + const testSuite: []const struct { Network, []const u8, []const u8, []const u8 } = &.{ + .{ + .MAINNET, + "m", + "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8", + }, + .{ + .MAINNET, + "m/0h", + "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", + "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw", + }, + .{ + .MAINNET, + "m/0h/1", + "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", + "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ", + }, + .{ + .MAINNET, + "m/0h/1/2h", + "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", + "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5", + }, + .{ + .MAINNET, + "m/0h/1/2h/2", + "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", + "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV", + }, + .{ + .MAINNET, + "m/0h/1/2h/2/1000000000", + "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", + "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy", + }, + }; + + for (testSuite, 0..) |suite, idx| { + errdefer { + std.log.warn("suite failed n={d} : {any}", .{ idx + 1, suite }); + } + + const path = try derivatePathFromStr(suite[1], std.testing.allocator); + defer path.deinit(); + + try testPath(secp, .MAINNET, seed, path.items, suite[2], suite[3]); + } +} + +test "vector_2" { + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + var buf: [100]u8 = undefined; + + const seed = try std.fmt.hexToBytes(&buf, "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542"); + // derivation path, expected_sk , expected_pk + const testSuite: []const struct { Network, []const u8, []const u8, []const u8 } = &.{ + .{ + .MAINNET, + "m", + "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", + "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", + }, + .{ + .MAINNET, + "m/0", + "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt", + "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", + }, + .{ + .MAINNET, + "m/0/2147483647h", + "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", + "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a", + }, + .{ + .MAINNET, + "m/0/2147483647h/1", + "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef", + "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon", + }, + .{ + .MAINNET, + "m/0/2147483647h/1/2147483646h", + "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", + "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL", + }, + .{ + .MAINNET, + "m/0/2147483647h/1/2147483646h/2", + "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", + "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt", + }, + }; + + for (testSuite, 0..) |suite, idx| { + errdefer { + std.log.warn("suite failed n={d} : {any}", .{ idx + 1, suite }); + } + + const path = try derivatePathFromStr(suite[1], std.testing.allocator); + defer path.deinit(); + + try testPath(secp, .MAINNET, seed, path.items, suite[2], suite[3]); + } +} + +test "vector_3" { + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + var buf: [100]u8 = undefined; + + const seed = try std.fmt.hexToBytes(&buf, "4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be"); + + const path_1 = try derivatePathFromStr("m", std.testing.allocator); + defer path_1.deinit(); + + // m + try testPath(secp, .MAINNET, seed, path_1.items, "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6", "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13"); + + // m/0h + const path_2 = try derivatePathFromStr("m/0h", std.testing.allocator); + defer path_2.deinit(); + + try testPath(secp, .MAINNET, seed, path_2.items, "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L", "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y"); +} + +test "base58_check_decode_encode" { + const encoded = try base58EncodeCheck(std.testing.allocator, "test"); + defer std.testing.allocator.free(encoded); + + const decoded = try base58DecodeCheck(std.testing.allocator, encoded); + defer std.testing.allocator.free(decoded); + + try std.testing.expectEqualSlices(u8, decoded, "test"); +} diff --git a/src/core/bip32/key.zig b/src/core/bip32/key.zig new file mode 100644 index 0000000..2ecc777 --- /dev/null +++ b/src/core/bip32/key.zig @@ -0,0 +1,12 @@ +const secp256k1 = @import("../secp256k1.zig"); +const Network = @import("bip32.zig").Network; + +/// A Bitcoin ECDSA private key +pub const PrivateKey = struct { + /// Whether this private key should be serialized as compressed + compressed: bool, + /// The network on which this key should be used + network: Network, + /// The actual ECDSA key + inner: secp256k1.SecretKey, +}; diff --git a/src/core/bip32/utils.zig b/src/core/bip32/utils.zig new file mode 100644 index 0000000..4280cd0 --- /dev/null +++ b/src/core/bip32/utils.zig @@ -0,0 +1,182 @@ +const std = @import("std"); +const math = std.math; +const unicode = std.unicode; +const base58 = @import("base58"); +const Ripemd160 = @import("crypto").Ripemd160; + +pub const DecodedCompactSize = struct { + totalBytes: u8, + n: u64, +}; + +pub const EncodedCompactSize = struct { + compactSizeByte: u8, + totalBytes: u8, + n: u64, +}; + +pub fn intToHexStr(comptime T: type, data: T, buffer: []u8) !void { + // Number of characters to represent data in hex + // log16(data) + 1 + const n: usize = if (data == 0) 1 else @intCast(math.log(T, 16, data) + 1); + const missing: usize = @intCast(buffer.len - n); + for (0..missing) |i| { + buffer[i] = '0'; + } + _ = try std.fmt.bufPrint(buffer[missing..], "{x}", .{data}); +} + +pub fn toBase58(buffer: []u8, bytes: []const u8) !void { + const encoder = base58.Encoder.init(.{}); + _ = try encoder.encode(bytes, buffer); +} + +pub fn toBase58Allocator(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 { + const encoder = base58.Encoder.init(.{}); + return try encoder.encodeAlloc(allocator, bytes); +} + +pub fn fromBase58(encoded: []const u8, buffer: []u8) !void { + const decoder = base58.Decoder.init(.{}); + _ = try decoder.decode(encoded, buffer); +} + +pub fn calculateChecksum(bytes: []const u8) [4]u8 { + var buffer: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(bytes, &buffer, .{}); + std.crypto.hash.sha2.Sha256.hash(&buffer, &buffer, .{}); + return buffer[0..4].*; +} + +pub fn verifyChecksum(bytes: []const u8, checksum: [4]u8) bool { + var buffer: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(bytes, &buffer, .{}); + std.crypto.hash.sha2.Sha256.hash(&buffer, &buffer, .{}); + + return std.mem.eql(u8, buffer[0..4], checksum[0..4]); +} + +pub fn debugPrintBytes(comptime len: u32, bytes: []const u8) void { + var buf: [len]u8 = undefined; + _ = std.fmt.bufPrint(&buf, "{x}", .{std.fmt.fmtSliceHexLower(bytes)}) catch unreachable; + std.debug.print("DEBUG PRINT BYTES: {s}\n", .{buf}); +} + +pub fn doubleSha256(bytes: []const u8) [32]u8 { + var buffer: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(bytes, &buffer, .{}); + std.crypto.hash.sha2.Sha256.hash(&buffer, &buffer, .{}); + return buffer; +} + +pub fn hash160(bytes: []const u8) [20]u8 { + var hashed: [32]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(bytes, &hashed, .{}); + const r = Ripemd160.hash(&hashed); + return r.bytes; +} + +pub fn encodeutf8(in: []const u8, buffer: []u8) !u16 { + const v = try unicode.Utf8View.init(in); + var it = v.iterator(); + var cur: u16 = 0; + while (it.nextCodepoint()) |codepoint| { + var b: [4]u8 = undefined; + const len: u16 = @as(u16, try unicode.utf8Encode(codepoint, &b)); + @memcpy(buffer[cur .. cur + len], b[0..len]); + cur += len; + } + return cur; +} + +pub fn decodeCompactSize(v: []u8) DecodedCompactSize { + return switch (v[0]) { + 0...252 => DecodedCompactSize{ .totalBytes = 1, .n = v[0] }, + 253 => { + const n = std.mem.readInt(u16, v[1..3], .big); + return DecodedCompactSize{ .totalBytes = 3, .n = n }; + }, + 254 => { + const n = std.mem.readInt(u32, v[1..5], .big); + return DecodedCompactSize{ .totalBytes = 5, .n = n }; + }, + 255 => { + const n = std.mem.readInt(u64, v[1..9], .big); + return DecodedCompactSize{ .totalBytes = 9, .n = n }; + }, + }; +} + +pub fn encodeCompactSize(n: u64) EncodedCompactSize { + return switch (n) { + 0...252 => EncodedCompactSize{ .compactSizeByte = @intCast(n), .totalBytes = 0, .n = n }, + 253...65535 => EncodedCompactSize{ .compactSizeByte = 253, .totalBytes = 2, .n = n }, + 65536...4294967295 => EncodedCompactSize{ .compactSizeByte = 254, .totalBytes = 4, .n = n }, + 4294967296...18446744073709551615 => EncodedCompactSize{ .compactSizeByte = 255, .totalBytes = 8, .n = n }, + }; +} + +pub fn reverseByteOrderFromHex(comptime size: usize, hex: [size]u8) ![size]u8 { + var bytes: [size / 2]u8 = undefined; + const bytes_size = size / 2; + _ = try std.fmt.hexToBytes(&bytes, &hex); + + for (0..bytes_size / 2) |i| { // size / 4 = bytes.len / 2 + bytes[i] = bytes[bytes_size - 1 - i] ^ bytes[i]; + bytes[bytes_size - 1 - i] = bytes[i] ^ bytes[bytes_size - 1 - i]; + bytes[i] = bytes[bytes_size - 1 - i] ^ bytes[i]; + } + + var result: [size]u8 = undefined; + _ = try std.fmt.bufPrint(&result, "{x}", .{std.fmt.fmtSliceHexLower(&bytes)}); + return result; +} + +test "intToHexStr" { + var buffer: [8]u8 = undefined; + try intToHexStr(u8, 150, &buffer); + try std.testing.expectEqualSlices(u8, buffer[0..], "00000096"); + try intToHexStr(u32, 4294967295, &buffer); + try std.testing.expectEqualSlices(u8, buffer[0..], "ffffffff"); + + var buffer2: [8]u8 = undefined; + try intToHexStr(u8, 0, &buffer2); + try std.testing.expectEqualSlices(u8, buffer2[0..], "00000000"); +} + +test "toBase58" { + const str = "00f57f296d748bb310dc0512b28231e8ebd62454557d5edaef".*; + var b: [25]u8 = undefined; + _ = try std.fmt.hexToBytes(&b, &str); + var base58_address: [34]u8 = undefined; + _ = try toBase58(&base58_address, &b); + try std.testing.expectEqualSlices(u8, base58_address[0..], "1PP4tMi6tep8qo8NwUDRaNw5cdiDVZYEnJ"); +} + +test "hash160" { + var str = "03525cbe17e87969013e6457c765594580dc803a8497052d7c1efb0ef401f68bd5".*; + var bytes: [33]u8 = undefined; + _ = try std.fmt.hexToBytes(&bytes, &str); + const r = hash160(bytes[0..]); + var rstr: [40]u8 = undefined; + _ = try std.fmt.bufPrint(&rstr, "{x}", .{std.fmt.fmtSliceHexLower(&r)}); + try std.testing.expectEqualStrings("286fd267876fb1a24b8fe798edbc6dc6d5e2ea5b", &rstr); +} + +test "reverseByteOrderFromHex" { + const hex1 = "7790b18693b2c4b6344577dc8d973e51388670a2b60ef1156b69f141f66b837e".*; + const expected1 = "7e836bf641f1696b15f10eb6a2708638513e978ddc774534b6c4b29386b19077".*; + const res1 = try reverseByteOrderFromHex(64, hex1); + + const hex2 = "4429cda513e5258a16f5be9fe6bf9d8f18aa7d8ca6e5147b10961955db88ac74".*; + const expected2 = "74ac88db551996107b14e5a68c7daa188f9dbfe69fbef5168a25e513a5cd2944".*; + const res2 = try reverseByteOrderFromHex(64, hex2); + + const hex3 = "396b7f0fcac84f700b471fc72874f56795433b7cb7657fe3ff9e9d0e573960a7".*; + const expected3 = "a76039570e9d9effe37f65b77c3b439567f57428c71f470b704fc8ca0f7f6b39".*; + const res3 = try reverseByteOrderFromHex(64, hex3); + + try std.testing.expectEqualStrings(&expected1, &res1); + try std.testing.expectEqualStrings(&expected2, &res2); + try std.testing.expectEqualStrings(&expected3, &res3); +} diff --git a/src/core/bip39/bip39.zig b/src/core/bip39/bip39.zig new file mode 100644 index 0000000..005e4f7 --- /dev/null +++ b/src/core/bip39/bip39.zig @@ -0,0 +1,352 @@ +//! # BIP39 Mnemonic Codes +//! +//! https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki +//! +const std = @import("std"); +const language = @import("language.zig"); +const pbkdf2 = @import("pbkdf2.zig"); + +/// The minimum number of words in a mnemonic. +const MIN_NB_WORDS: usize = 12; + +/// The maximum number of words in a mnemonic. +const MAX_NB_WORDS: usize = 24; + +/// The index used to indicate the mnemonic ended. +const EOF: u16 = std.math.maxInt(u16); + +/// A mnemonic code. +/// +/// The [core::str::FromStr] implementation will try to determine the language of the +/// mnemonic from all the supported languages. (Languages have to be explicitly enabled using +/// the Cargo features.) +/// +/// Supported number of words are 12, 15, 18, 21, and 24. +pub const Mnemonic = struct { + /// The language the mnemonic. + lang: language.Language, + /// The indiced of the words. + /// Mnemonics with less than the max nb of words are terminated with EOF. + words: [MAX_NB_WORDS]u16, + + /// Parse a mnemonic in normalized UTF8 in the given language. + pub fn parseInNormalized(lang: language.Language, s: []const u8) !Mnemonic { + var it = std.mem.splitScalar(u8, s, ' '); + var nb_words: usize = 0; + + while (it.next()) |_| nb_words += 1; + it.reset(); + + if (isInvalidWordCount(nb_words)) { + return error.BadWordCount; + } + + // Here we will store the eventual words. + var words = [_]u16{EOF} ** MAX_NB_WORDS; + + // And here we keep track of the bits to calculate and validate the checksum. + // We only use `nb_words * 11` elements in this array. + var bits = [_]bool{false} ** (MAX_NB_WORDS * 11); + + { + var i: usize = 0; + while (it.next()) |word| { + const idx = lang.findWord(word) orelse return error.UnknownWord; + + words[i] = idx; + + for (0..11) |j| { + bits[i * 11 + j] = std.math.shr(u16, idx, 10 - j) & 1 == 1; + } + i += 1; + } + } + + // Verify the checksum. + // We only use `nb_words / 3 * 4` elements in this array. + + var entropy = [_]u8{0} ** (MAX_NB_WORDS / 3 * 4); + const nb_bytes_entropy = nb_words / 3 * 4; + for (0..nb_bytes_entropy) |i| { + for (0..8) |j| { + if (bits[i * 8 + j]) { + entropy[i] += std.math.shl(u8, 1, 7 - j); + } + } + } + + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + hasher.update(entropy[0..nb_bytes_entropy]); + + const check = hasher.finalResult(); + + for (0..nb_bytes_entropy / 4) |i| { + if (bits[8 * nb_bytes_entropy + i] != ((check[i / 8] & (std.math.shl(usize, 1, 7 - (i % 8)))) > 0)) { + return error.InvalidChecksum; + } + } + + return .{ + .lang = lang, + .words = words, + }; + } + + /// Convert to seed bytes with a passphrase in normalized UTF8. + pub fn toSeedNormalized(self: Mnemonic, normalized_passphrase: []const u8) ![64]u8 { + const PBKDF2_ROUNDS: usize = 2048; + const PBKDF2_BYTES: usize = 64; + + var seed = [_]u8{0} ** PBKDF2_BYTES; + + pbkdf2.pbkdf2((try self.getWords()).slice(), normalized_passphrase, PBKDF2_ROUNDS, &seed); + return seed; + } + + /// Returns an slice over [Mnemonic] word indices. + /// + pub fn wordIndices(self: Mnemonic) !std.BoundedArray(u16, MAX_NB_WORDS) { + var result = try std.BoundedArray(u16, MAX_NB_WORDS).init(0); + + for (self.words) |w| { + if (w != EOF) { + result.appendAssumeCapacity(w); + continue; + } + + break; + } + + return result; + } + + /// Returns an iterator over the words of the [Mnemonic]. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// const bip39 = @import("bip39"); + /// + /// const mnemonic = try bip39.Mnemonic.fromEntropy(&([_]u8{0} ** 32)); + /// for (mnemonic.words()) |word| { + /// std.log.debug("word: {s}", .{word}); + /// } + /// ``` + pub fn getWords(self: Mnemonic) !std.BoundedArray([]const u8, MAX_NB_WORDS) { + const list = self.lang.wordList(); + const word_indices = try self.wordIndices(); + + var result = try std.BoundedArray([]const u8, MAX_NB_WORDS).init(0); + + for (word_indices.slice()) |i| { + result.appendAssumeCapacity(list[i]); + } + + return result; + } + + /// Create a new [Mnemonic] in the specified language from the given entropy. + /// Entropy must be a multiple of 32 bits (4 bytes) and 128-256 bits in length. + pub fn fromEntropyIn(lang: language.Language, entropy: []const u8) !Mnemonic { + const MAX_ENTROPY_BITS: usize = 256; + const MIN_ENTROPY_BITS: usize = 128; + const MAX_CHECKSUM_BITS: usize = 8; + + const nb_bytes = entropy.len; + const nb_bits = nb_bytes * 8; + + if (nb_bits % 32 != 0) { + return error.BadEntropyBitCount; + } + + if (nb_bits < MIN_ENTROPY_BITS or nb_bits > MAX_ENTROPY_BITS) { + return error.BadEntropyBitCount; + } + + const check = v: { + var out: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(entropy, &out, .{}); + break :v out; + }; + + var bits = [_]bool{false} ** (MAX_ENTROPY_BITS + MAX_CHECKSUM_BITS); + + for (0..nb_bytes) |i| { + for (0..8) |j| { + bits[i * 8 + j] = (entropy[i] & (std.math.shl(usize, 1, 7 - j))) > 0; + } + } + + for (0..nb_bytes / 4) |i| { + bits[8 * nb_bytes + i] = (check[i / 8] & (std.math.shl(usize, 1, 7 - (i % 8)))) > 0; + } + + var words = [_]u16{EOF} ** MAX_NB_WORDS; + const nb_words = nb_bytes * 3 / 4; + for (0..nb_words) |i| { + var idx: u16 = 0; + for (0..11) |j| { + if (bits[i * 11 + j]) { + idx += std.math.shl(u16, 1, @as(u16, @truncate(10 - j))); + } + } + + words[i] = idx; + } + + return .{ + .lang = lang, + .words = words, + }; + } +}; + +fn isInvalidWordCount(word_count: usize) bool { + return word_count < MIN_NB_WORDS or word_count % 3 != 0 or word_count > MAX_NB_WORDS; +} + +test "english_vectors" { + // These vectors are tuples of + // (entropy, mnemonic, seed) + + const test_vectors = [_]struct { []const u8, []const u8, []const u8 }{ + .{ + "00000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", + "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", + }, + .{ + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank yellow", + "2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607", + }, + .{ + "80808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above", + "d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8", + }, + .{ + "ffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong", + "ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069", + }, + .{ + "000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", + "035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa", + }, + .{ + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will", + "f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd", + }, + .{ + "808080808080808080808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always", + "107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65", + }, + .{ + "ffffffffffffffffffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when", + "0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528", + }, + .{ + "0000000000000000000000000000000000000000000000000000000000000000", + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", + "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8", + }, + .{ + "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", + "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title", + "bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87", + }, + .{ + "8080808080808080808080808080808080808080808080808080808080808080", + "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless", + "c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f", + }, + .{ + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", + "dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad", + }, + .{ + "9e885d952ad362caeb4efe34a8e91bd2", + "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic", + "274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028", + }, + .{ + "6610b25967cdcca9d59875f5cb50b0ea75433311869e930b", + "gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog", + "628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac", + }, + .{ + "68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c", + "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length", + "64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440", + }, + .{ + "c0ba5a8e914111210f2bd131f3d5e08d", + "scheme spot photo card baby mountain device kick cradle pact join borrow", + "ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612", + }, + .{ + "6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3", + "horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave", + "fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d", + }, + .{ + "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863", + "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside", + "72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d", + }, + .{ + "23db8160a31d3e0dca3688ed941adbf3", + "cat swing flag economy stadium alone churn speed unique patch report train", + "deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5", + }, + .{ + "8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0", + "light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access", + "4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02", + }, + .{ + "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad", + "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", + "26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d", + }, + .{ + "f30f8c1da665478f49b001d94c5fc452", + "vessel ladder alter error federal sibling chat ability sun glass valve picture", + "2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f", + }, + .{ + "c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05", + "scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump", + "7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88", + }, + .{ + "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f", + "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold", + "01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998", + }, + }; + + var buf: [300]u8 = undefined; + + for (test_vectors) |vector| { + const entropy = try std.fmt.hexToBytes(&buf, vector[0]); + const mn = try Mnemonic.fromEntropyIn(.english, entropy); + const mn1 = try Mnemonic.parseInNormalized(.english, vector[1]); + + try std.testing.expectEqualDeep(mn, mn1); + + const seed = try std.fmt.hexToBytes(buf[100..], vector[2]); + + const seeded = try mn.toSeedNormalized("TREZOR"); + + try std.testing.expectEqualSlices(u8, &seeded, seed); + } +} diff --git a/src/core/bip39/language.zig b/src/core/bip39/language.zig new file mode 100644 index 0000000..22ac827 --- /dev/null +++ b/src/core/bip39/language.zig @@ -0,0 +1,2124 @@ +const std = @import("std"); + +/// The maximum number of languages enabled. +pub const MAX_NB_LANGUAGES: usize = 9; + +pub const Language = enum { + /// The English language. + english, + + pub inline fn wordList(self: Language) *const [2048][]const u8 { + return switch (self) { + inline .english => return &ENGLISH_WORDS, + }; + } + + /// Returns true if all words in the list are guaranteed to + /// only be in this list and not in any other. + pub inline fn uniqueWords(self: Language) bool { + return switch (self) { + inline .english => false, + }; + } + + /// Get words from the word list that start with the given prefix. + pub fn wordsByPrefix(self: Language, prefix: []const u8) ?[]const []const u8 { + // The words in the word list are ordered lexicographically. This means + // that we cannot use `binary_search` to find words more efficiently, + // because the Rust ordering is based on the byte values. However, it + // does mean that words that share a prefix will follow each other. + var start_from: usize = 0; + var count: usize = 0; + + const word_list = self.wordList(); + for (word_list.*, 0..) |w, idx| { + if (std.mem.startsWith(u8, w, prefix)) { + count = 1; + start_from = idx; + + for (idx + 1..2048) |i| { + if (!std.mem.startsWith(u8, word_list.*[i], prefix)) break; + + count += 1; + } + break; + } + } + + if (count == 0) return null; + + return word_list[start_from .. start_from + count]; + } + + /// Get the index of the word in the word list. + pub inline fn findWord(self: Language, word: []const u8) ?u16 { + for (self.wordList(), 0..) |w, i| { + if (std.mem.eql(u8, w, word)) return @truncate(i); + } + + return null; + } +}; + +const ENGLISH_WORDS: [2048][]const u8 = .{ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo", +}; + +test "words_by_prefix" { + const lang: Language = .english; + + var res = lang.wordsByPrefix("woo") orelse @panic("not expect"); + try std.testing.expectEqualSlices([]const u8, &.{ "wood", "wool" }, res); + + res = lang.wordsByPrefix("") orelse @panic("not expect"); + try std.testing.expectEqual(res.len, 2048); + + try std.testing.expect(lang.wordsByPrefix("woof") == null); +} diff --git a/src/core/bip39/pbkdf2.zig b/src/core/bip39/pbkdf2.zig new file mode 100644 index 0000000..5f773f3 --- /dev/null +++ b/src/core/bip39/pbkdf2.zig @@ -0,0 +1,143 @@ +const std = @import("std"); +const SALT_PREFIX = "mnemonic"; + +const Hmac = std.crypto.auth.hmac.sha2.HmacSha512; +const Sha512 = std.crypto.hash.sha2.Sha512; + +/// Calculate the binary size of the mnemonic. +fn mnemonicByteLen(mnemonic: []const []const u8) usize { + var len: usize = 0; + for (0.., mnemonic) |i, word| { + if (i > 0) { + len += 1; + } + + len += word.len; + } + return len; +} + +/// Wrote the mnemonic in binary form into the hash engine. +fn mnemonicWriteInto(mnemonic: []const []const u8, engine: *Sha512) void { + for (0.., mnemonic) |i, word| { + if (i > 0) { + engine.update(" "); + } + engine.update(word); + } +} + +/// Create an HMAC engine from the passphrase. +/// We need a special method because we can't allocate a new byte +/// vector for the entire serialized mnemonic. +fn createHmacEngine(mnemonic: []const []const u8) Hmac { + // Inner code is borrowed from the bitcoin_hashes::hmac::HmacEngine::new method. + var ipad = [_]u8{0x36} ** 128; + var opad = [_]u8{0x5c} ** 128; + + var iengine = Sha512.init(.{}); + + if (mnemonicByteLen(mnemonic) > Sha512.block_length) { + const hash = v: { + var engine = Sha512.init(.{}); + mnemonicWriteInto(mnemonic, &engine); + var final: [Sha512.digest_length]u8 = undefined; + engine.final(&final); + break :v final; + }; + + for (ipad[0..64], hash) |*b_i, b_h| { + b_i.* = b_i.* ^ b_h; + } + + for (opad[0..64], hash) |*b_o, b_h| { + b_o.* = b_o.* ^ b_h; + } + } else { + // First modify the first elements from the prefix. + var cursor: usize = 0; + for (0.., mnemonic) |i, word| { + if (i > 0) { + ipad[cursor] ^= ' '; + opad[cursor] ^= ' '; + cursor += 1; + } + + const min_len = @min(ipad.len - cursor, word.len); + for (ipad[cursor .. cursor + min_len], word[0..min_len]) |*b_i, b_h| { + b_i.* = b_i.* ^ b_h; + } + + for (opad[cursor .. cursor + min_len], word[0..min_len]) |*b_o, b_h| { + b_o.* = b_o.* ^ b_h; + } + + cursor += word.len; + // assert!(cursor <= sha512::HashEngine::BLOCK_SIZE, "mnemonic_byte_len is broken"); + } + } + + iengine.update(ipad[0..Sha512.block_length]); + + return Hmac{ + .o_key_pad = opad[0..Sha512.block_length].*, + .hash = iengine, + }; +} + +inline fn xor(res: []u8, salt: []const u8) void { + // length mismatch in xor + std.debug.assert(salt.len >= res.len); + const min_len = @min(res.len, salt.len); + for (res[0..min_len], salt[0..min_len]) |*a, b| { + a.* = a.* ^ b; + } +} + +/// PBKDF2-HMAC-SHA512 implementation using bitcoin_hashes. +pub fn pbkdf2(mnemonic: []const []const u8, unprefixed_salt: []const u8, c: usize, res: []u8) void { + const prf = createHmacEngine(mnemonic); + @memset(res, 0); + + // var pprf = prf; + + // var prf_buf: [Hmac.mac_length]u8 = undefined; + // pprf.final(&prf_buf); + + // std.log.warn("pprf :{any}", .{prf_buf}); + + var i: usize = 0; + + while (i < res.len) : ({ + i += Sha512.digest_length; + }) { + const chunk_too = @min(res.len, i + Sha512.digest_length); + const chunk: []u8 = res[i..chunk_too]; + var salt = v: { + var prfc = prf; + prfc.update(SALT_PREFIX); + prfc.update(unprefixed_salt); + + var buf: [4]u8 = undefined; + std.mem.writeInt(u32, &buf, @truncate(i + 1), .big); + + prfc.update(&buf); + + var salt: [Hmac.mac_length]u8 = undefined; + + prfc.final(&salt); + + xor(chunk, &salt); + break :v salt; + }; + + for (1..c) |_| { + var prfc = prf; + + prfc.update(&salt); + + prfc.final(&salt); + xor(chunk, &salt); + } + } +} diff --git a/src/core/blind.zig b/src/core/blind.zig index df80c27..5f0c369 100644 --- a/src/core/blind.zig +++ b/src/core/blind.zig @@ -38,7 +38,7 @@ pub const BlindedSignature = struct { pub const BlindedMessage = struct { amount: u64, b_: secp256k1.PublicKey, - id: []const u8, + id: [16]u8, pub usingnamespace @import("../helper/helper.zig").RenameJsonField( @This(), @@ -68,7 +68,7 @@ test "blind serialize" { const sig = BlindedSignature{ .amount = 10, .c_ = pub_key, - .id = "dfdfdf", + .id = undefined, }; const json = try std.json.stringifyAlloc(std.testing.allocator, &sig, .{}); @@ -77,13 +77,11 @@ test "blind serialize" { const parsedSig = try std.json.parseFromSlice(BlindedSignature, std.testing.allocator, json, .{}); defer parsedSig.deinit(); - try std.testing.expectEqual(sig.amount, parsedSig.value.amount); - try std.testing.expectEqualSlices(u8, sig.id, parsedSig.value.id); - try std.testing.expectEqualSlices(u8, &sig.c_.pk.data, &parsedSig.value.c_.pk.data); + try std.testing.expectEqual(sig, parsedSig.value); const msg = BlindedMessage{ .amount = 11, - .id = "dfdfdf", + .id = undefined, .b_ = pub_key, }; @@ -93,7 +91,5 @@ test "blind serialize" { const parsedMsg = try std.json.parseFromSlice(BlindedMessage, std.testing.allocator, json_msg, .{}); defer parsedMsg.deinit(); - try std.testing.expectEqual(msg.amount, parsedMsg.value.amount); - try std.testing.expectEqualSlices(u8, msg.id, parsedMsg.value.id); - try std.testing.expectEqualSlices(u8, &msg.b_.pk.data, &parsedMsg.value.b_.pk.data); + try std.testing.expectEqual(msg, parsedMsg.value); } diff --git a/src/core/dhke.zig b/src/core/dhke.zig new file mode 100644 index 0000000..3d9e1ea --- /dev/null +++ b/src/core/dhke.zig @@ -0,0 +1,200 @@ +//! Diffie-Hellmann key exchange +const std = @import("std"); +const secp256k1 = @import("secp256k1.zig"); +const secret_lib = @import("secret.zig"); +const nuts = @import("nuts/lib.zig"); + +const DOMAIN_SEPARATOR = "Secp256k1_HashToCurve_Cashu_"; + +/// Deterministically maps a message to a public key point on the secp256k1 curve, utilizing a domain separator to ensure uniqueness. +/// +/// For definationn in NUT see [NUT-00](https://github.com/cashubtc/nuts/blob/main/00.md) +pub fn hashToCurve(message: []const u8) !secp256k1.PublicKey { + const domain_separator = "Secp256k1_HashToCurve_Cashu_"; + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + + hasher.update(domain_separator); + hasher.update(message); + + const msg_to_hash = hasher.finalResult(); + + var buf: [33]u8 = undefined; + buf[0] = 0x02; + + var counter_buf: [4]u8 = undefined; + + const till = comptime try std.math.powi(u32, 2, 16); + + var counter: u32 = 0; + + while (counter < till) : (counter += 1) { + hasher = std.crypto.hash.sha2.Sha256.init(.{}); + + hasher.update(&msg_to_hash); + std.mem.writeInt(u32, &counter_buf, counter, .little); + hasher.update(&counter_buf); + hasher.final(buf[1..]); + + const pk = secp256k1.PublicKey.fromSlice(&buf) catch continue; + + return pk; + } + + return error.NoValidPointFound; +} + +/// Convert iterator of [`PublicKey`] to byte array +pub fn hashE(public_keys: []const secp256k1.PublicKey) [32]u8 { + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + + for (public_keys) |pk| { + const uncompressed = pk.serializeUncompressed(); + hasher.update(&std.fmt.bytesToHex(uncompressed, .lower)); + } + + return hasher.finalResult(); +} + +/// Blind Message +/// +/// `B_ = Y + rG` +pub fn blindMessage( + secp: secp256k1.Secp256k1, + secret: []const u8, + blinding_factor: ?secp256k1.SecretKey, +) !struct { secp256k1.PublicKey, secp256k1.SecretKey } { + const y = try hashToCurve(secret); + const r = blinding_factor orelse secp256k1.SecretKey.generate(); + + return .{ try y.combine(r.publicKey(secp)), r }; +} + +/// Unblind Message +/// +/// `C_ - rK` +pub fn unblindMessage( + secp: secp256k1.Secp256k1, + // C_ + blinded_key: secp256k1.PublicKey, + _r: secp256k1.SecretKey, + // K + mint_pubkey: secp256k1.PublicKey, +) !secp256k1.PublicKey { + const r = secp256k1.Scalar.fromSecretKey(_r); + + // a = r * K + var a = try mint_pubkey.mulTweak(&secp, r); + + // C_ - a + a = a.negate(&secp); + + return try blinded_key.combine(a); +} + +/// Construct Proof +pub fn constructProofs( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + promises: []const nuts.BlindSignature, + rs: []const secp256k1.SecretKey, + secrets: []const secret_lib.Secret, + keys: nuts.Keys, +) !std.ArrayList(nuts.Proof) { + var proofs = std.ArrayList(nuts.Proof).init(allocator); + errdefer proofs.deinit(); + + for (promises, rs, secrets) |blinded_signature, r, secret| { + const blinded_c = blinded_signature.c; + + const a = keys.amountKey(blinded_signature.amount) orelse return error.CantGetProofs; + + const unblinded_signature = try unblindMessage(secp, blinded_c, r, a); + + const dleq: ?nuts.ProofDleq = if (blinded_signature.dleq) |d| + nuts.ProofDleq{ + .e = d.e, + .s = d.s, + .r = r, + } + else + null; + try proofs.append(nuts.Proof{ + .amount = blinded_signature.amount, + .keyset_id = blinded_signature.keyset_id, + .secret = secret, + .c = unblinded_signature, + .witness = null, + .dleq = dleq, + }); + } + + return proofs; +} + +/// Sign Blinded Message +/// +/// `C_ = k * B_`, where: +/// * `k` is the private key of mint (one for each amount) +/// * `B_` is the blinded message +pub inline fn signMessage(secp: secp256k1.Secp256k1, k: secp256k1.SecretKey, blinded_message: secp256k1.PublicKey) !secp256k1.PublicKey { + const _k = secp256k1.Scalar.fromSecretKey(k); + return try blinded_message.mulTweak(&secp, _k); +} + +/// Verify Message +pub fn verifyMessage( + secp: secp256k1.Secp256k1, + a: secp256k1.SecretKey, + unblinded_message: secp256k1.PublicKey, + msg: []const u8, +) !void { + // Y + const y = try hashToCurve(msg); + + // Compute the expected unblinded message + const expected_unblinded_message = try y + .mulTweak(&secp, secp256k1.Scalar.fromSecretKey(a)); + + // Compare the unblinded_message with the expected value + if (unblinded_message.eql(expected_unblinded_message)) + return; + + return error.TokenNotVerified; +} + +test "test_hash_to_curve" { + var hex_buffer: [100]u8 = undefined; + + var secret = "0000000000000000000000000000000000000000000000000000000000000000"; + + var sec_hex = try std.fmt.hexToBytes(&hex_buffer, secret); + + var y = try hashToCurve(sec_hex); + var expected_y = try secp256k1.PublicKey.fromString( + "024cce997d3b518f739663b757deaec95bcd9473c30a14ac2fd04023a739d1a725", + ); + + try std.testing.expectEqual(y, expected_y); + + secret = "0000000000000000000000000000000000000000000000000000000000000001"; + + sec_hex = try std.fmt.hexToBytes(&hex_buffer, secret); + + y = try hashToCurve(sec_hex); + expected_y = try secp256k1.PublicKey.fromString( + "022e7158e11c9506f1aa4248bf531298daa7febd6194f003edcd9b93ade6253acf", + ); + + try std.testing.expectEqual(y, expected_y); + + secret = "0000000000000000000000000000000000000000000000000000000000000002"; + + sec_hex = try std.fmt.hexToBytes(&hex_buffer, secret); + + y = try hashToCurve(sec_hex); + expected_y = try secp256k1.PublicKey.fromString( + "026cdbe15362df59cd1dd3c9c11de8aedac2106eca69236ecd9fbe117af897be4f", + ); + + try std.testing.expectEqual(y, expected_y); +} diff --git a/src/core/keyset.zig b/src/core/keyset.zig deleted file mode 100644 index 6e838cc..0000000 --- a/src/core/keyset.zig +++ /dev/null @@ -1,356 +0,0 @@ -const std = @import("std"); - -const Secp256k1 = @import("secp256k1.zig").Secp256k1; -const SecretKey = @import("secp256k1.zig").SecretKey; -const PublicKey = @import("secp256k1.zig").PublicKey; -const primitives = @import("primitives.zig"); -const MAX_ORDER: u64 = 64; - -pub const MintKeyset = struct { - private_keys: std.AutoHashMap(u64, SecretKey), - public_keys: std.AutoHashMap(u64, PublicKey), - keyset_id: [16]u8, - mint_pubkey: PublicKey, - - pub fn init(allocator: std.mem.Allocator, seed: []const u8, derivation_path: []const u8) !MintKeyset { - var priv_keys = try deriveKeys(allocator, seed, derivation_path); - errdefer priv_keys.deinit(); - - var pub_keys = try derivePubkeys(allocator, priv_keys); - errdefer pub_keys.deinit(); - - const keyset_id = try deriveKeysetId(allocator, pub_keys); - - const mint_pubkey = derivePubkey(allocator, seed) catch return error.InvalidSeed; - - return .{ - .private_keys = priv_keys, - .keyset_id = keyset_id, - .public_keys = pub_keys, - .mint_pubkey = mint_pubkey, - }; - } - - pub fn deinit(self: *@This()) void { - self.private_keys.deinit(); - self.public_keys.deinit(); - } -}; - -pub const Keyset = struct { - id: [16]u8, - unit: primitives.CurrencyUnit, - active: bool, -}; - -pub const Keysets = struct { - keysets: []const Keyset, -}; - -const sha256 = std.crypto.hash.sha2.Sha256; - -/// Derives a set of secret keys from a master key using a given derivation path. -/// -/// # Arguments -/// -/// * `allocator` - A allocator that allocate result hash map. -/// * `master_key` - A string slice that holds the master key. -/// * `derivation_path` - A string slice that holds the derivation path. -/// -/// # Returns -/// -/// A HashMap containing the derived secret keys, where the key is a u64 value and the value is a SecretKey. -pub fn deriveKeys(allocator: std.mem.Allocator, master_key: []const u8, derivation_path: []const u8) !std.AutoHashMap(u64, SecretKey) { - var keys = std.AutoHashMap(u64, SecretKey).init(allocator); - errdefer keys.deinit(); - - var hash_buffer: [sha256.digest_length]u8 = undefined; - // var usize_buf: [10]u8 = undefined; - - for (0..MAX_ORDER) |i| { - var hasher = sha256.init(.{}); - - // hasher.update(master_key); - // hasher.update(derivation_path); - try std.fmt.format(hasher.writer(), "{s}{s}{d}", .{ master_key, derivation_path, i }); - // sha256.init(.{}); - - hasher.final(&hash_buffer); - const key = try SecretKey.fromSlice(&hash_buffer); - - try keys.put(try std.math.powi(u64, 2, i), key); - } - - return keys; -} - -/// Derives public keys from a given set of secret keys. -/// -/// # Arguments -/// -/// * `allocator` - A allocator that allocate result hash map. -/// * `keys` - A HashMap containing the secret keys to derive public keys from. -/// -/// # Returns -/// -/// A HashMap containing the derived public keys. -pub fn derivePubkeys(allocator: std.mem.Allocator, keys: std.AutoHashMap(u64, SecretKey)) !std.AutoHashMap(u64, PublicKey) { - const secp = try Secp256k1.genNew(allocator); - defer secp.deinit(allocator); - - var res = std.AutoHashMap(u64, PublicKey).init(allocator); - errdefer res.deinit(); - var it = keys.iterator(); - - while (it.next()) |e| - try res.put(e.key_ptr.*, e.value_ptr.*.publicKey(secp)); - - return res; -} - -pub fn deriveKeysetId(allocator: std.mem.Allocator, keys: std.AutoHashMap(u64, PublicKey)) ![16]u8 { - var keys_arr = try std.ArrayList(u64).initCapacity(allocator, keys.count()); - defer keys_arr.deinit(); - - var it = keys.keyIterator(); - while (it.next()) |k| { - keys_arr.appendAssumeCapacity(k.*); - } - - std.sort.block(u64, keys_arr.items, {}, std.sort.asc(u64)); - - var hasher = sha256.init(.{}); - - for (keys_arr.items) |k| { - const pk = keys.get(k).?; - hasher.update(&pk.serialize()); - } - - const hashed_pubkeys = std.fmt.bytesToHex(hasher.finalResult(), .lower); - - var buf: [16]u8 = undefined; - buf[0] = '0'; - buf[1] = '0'; - - buf[2..16].* = hashed_pubkeys[0..14].*; - return buf; -} - -// stringifyMapOfPubkeys - stringify to json map of pub keys, caller own result json slice, should deallocate it through passed allocator -pub fn stringifyMapOfPubkeys(allocator: std.mem.Allocator, pub_keys: std.AutoHashMap(u64, PublicKey)) ![]u8 { - var result = std.ArrayList(u8).init(allocator); - errdefer result.deinit(); - - var ws = std.json.writeStream(result.writer(), .{}); - errdefer ws.deinit(); - - try ws.beginObject(); - - var buf: [25]u8 = undefined; - - var it = pub_keys.iterator(); - while (it.next()) |e| { - const n = std.fmt.formatIntBuf(&buf, e.key_ptr.*, 10, .lower, .{}); - - try ws.objectField(buf[0..n]); - try ws.write(e.value_ptr.*); - } - - try ws.endObject(); - - return try result.toOwnedSlice(); -} - -pub fn stringifyMapOfPubkeysWriter(out: anytype, pub_keys: std.AutoHashMap(u64, PublicKey)) !void { - try out.beginObject(); - - var buf: [25]u8 = undefined; - - var it = pub_keys.iterator(); - while (it.next()) |e| { - const n = std.fmt.formatIntBuf(&buf, e.key_ptr.*, 10, .lower, .{}); - - try out.objectField(buf[0..n]); - try out.write(e.value_ptr.*); - } - - try out.endObject(); -} - -pub fn parseMapOfPubkeys(allocator: std.mem.Allocator, json: []const u8) !std.AutoHashMap(u64, PublicKey) { - var scanner = std.json.Scanner.initCompleteInput(allocator, json); - defer scanner.deinit(); - - var res = std.AutoHashMap(u64, PublicKey).init(allocator); - errdefer res.deinit(); - - if (try scanner.next() != .object_begin) return error.UnexpectedToken; - - while (true) { - switch (try scanner.next()) { - .string => |key| { - const k = try std.fmt.parseInt(u64, key, 10); - - const pk = try std.json.innerParse(PublicKey, allocator, &scanner, .{}); - - try res.put(k, pk); - }, - .object_end => break, - else => return error.UnexpectedToken, - } - } - - return res; -} - -/// -/// # Arguments -/// -/// * `seed` - A string slice representing the seed to derive the public key from. -/// -/// # Returns -/// -/// Returns a `Result` containing the derived `PublicKey` or a `MokshaCoreError` if an error occurs. -pub fn derivePubkey(allocator: std.mem.Allocator, seed: []const u8) !PublicKey { - var hasher = sha256.init(.{}); - hasher.update(seed); - - const hash = hasher.finalResult(); - const key = try SecretKey.fromSlice(&hash); - - const secp = try Secp256k1.genNew(allocator); - defer secp.deinit(allocator); - - return key.publicKey(secp); -} - -test "test_derive_pubkey" { - const result = try derivePubkey(std.testing.allocator, "supersecretprivatekey"); - - try std.testing.expectEqualSlices(u8, &result.toString(), "03a2118b421e6b47f0656b97bb7eeea43c41096adbc0d0e511ff70de7d94dbd990"); -} - -test "test_derive_keys_master_v1" { - var keys = try deriveKeys(std.testing.allocator, "supersecretprivatekey", ""); - defer keys.deinit(); - - try std.testing.expectEqual(64, keys.count()); - - var pub_keys = try derivePubkeys(std.testing.allocator, keys); - defer pub_keys.deinit(); - - const id = try deriveKeysetId(std.testing.allocator, pub_keys); - try std.testing.expectEqualSlices(u8, "00d31cecf59d18c0", &id); -} - -test "test_derive_keyset_id" { - const keys_json = - \\{ - \\ "1":"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc", - \\ "2":"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de", - \\ "4":"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303", - \\ "8":"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528" - \\} - ; - - var pubs = try parseMapOfPubkeys(std.testing.allocator, keys_json); - defer pubs.deinit(); - - const keyset_id = try deriveKeysetId(std.testing.allocator, pubs); - - try std.testing.expectEqualSlices(u8, "00456a94ab4e1c46", &keyset_id); -} - -test "test_derive_keyset_id_long" { - const keys_json = - \\{ - \\ "1":"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566", - \\ "2":"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5", - \\ "4":"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7", - \\ "8":"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0", - \\ "16":"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d", - \\ "32":"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612", - \\ "64":"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664", - \\ "128":"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9", - \\ "256":"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459", - \\ "512":"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb", - \\ "1024":"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc", - \\ "2048":"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b", - \\ "4096":"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2", - \\ "8192":"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21", - \\ "16384":"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50", - \\ "32768":"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04", - \\ "65536":"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d", - \\ "131072":"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41", - \\ "262144":"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328", - \\ "524288":"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86", - \\ "1048576":"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788", - \\ "2097152":"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c", - \\ "4194304":"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512", - \\ "8388608":"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0", - \\ "16777216":"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21", - \\ "33554432":"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262", - \\ "67108864":"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3", - \\ "134217728":"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020", - \\ "268435456":"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276", - \\ "536870912":"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9", - \\ "1073741824":"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee", - \\ "2147483648":"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a", - \\ "4294967296":"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5", - \\ "8589934592":"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3", - \\ "17179869184":"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9", - \\ "34359738368":"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75", - \\ "68719476736":"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754", - \\ "137438953472":"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6", - \\ "274877906944":"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a", - \\ "549755813888":"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785", - \\ "1099511627776":"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a", - \\ "2199023255552":"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258", - \\ "4398046511104":"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a", - \\ "8796093022208":"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e", - \\ "17592186044416":"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310", - \\ "35184372088832":"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06", - \\ "70368744177664":"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1", - \\ "140737488355328":"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed", - \\ "281474976710656":"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d", - \\ "562949953421312":"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a", - \\ "1125899906842624":"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9", - \\ "2251799813685248":"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f", - \\ "4503599627370496":"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73", - \\ "9007199254740992":"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49", - \\ "18014398509481984":"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6", - \\ "36028797018963968":"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0", - \\ "72057594037927936":"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd", - \\ "144115188075855872":"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a", - \\ "288230376151711744":"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc", - \\ "576460752303423488":"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a", - \\ "1152921504606846976":"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06", - \\ "2305843009213693952":"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099", - \\ "4611686018427387904":"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f", - \\ "9223372036854775808":"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad" - \\ } - ; - - var pubs = try parseMapOfPubkeys(std.testing.allocator, keys_json); - defer pubs.deinit(); - - const keyset_id = try deriveKeysetId(std.testing.allocator, pubs); - - try std.testing.expectEqualSlices(u8, "000f01df73ea149a", &keyset_id); - - // checking encoding / decoding - const json = try stringifyMapOfPubkeys(std.testing.allocator, pubs); - defer std.testing.allocator.free(json); - - var pubs_dec = try parseMapOfPubkeys(std.testing.allocator, json); - defer pubs_dec.deinit(); - - try std.testing.expectEqual(pubs.count(), pubs_dec.count()); - - var it = pubs_dec.iterator(); - - while (it.next()) |e| { - const v = pubs.get(e.key_ptr.*) orelse return error.KeyNotFound; - try std.testing.expectEqual(v, e.value_ptr.*); - } -} diff --git a/src/core/lib.zig b/src/core/lib.zig index 335f023..4d3257c 100644 --- a/src/core/lib.zig +++ b/src/core/lib.zig @@ -1,6 +1,7 @@ -pub usingnamespace @import("bdhke.zig"); -pub usingnamespace @import("blind.zig"); - -pub const keyset = @import("keyset.zig"); -pub const primitives = @import("primitives.zig"); -pub const proof = @import("proof.zig"); +pub const secp256k1 = @import("secp256k1.zig"); +pub const bip32 = @import("bip32/bip32.zig"); +pub const dhke = @import("dhke.zig"); +pub const secret = @import("secret.zig"); +pub const amount = @import("amount.zig"); +pub const nuts = @import("nuts/lib.zig"); +pub const bip39 = @import("bip39/bip39.zig"); diff --git a/src/core/nuts/lib.zig b/src/core/nuts/lib.zig new file mode 100644 index 0000000..bb453f5 --- /dev/null +++ b/src/core/nuts/lib.zig @@ -0,0 +1,17 @@ +pub const nut15 = @import("nut15/nut15.zig"); +pub usingnamespace @import("nut14/nut14.zig"); +pub usingnamespace @import("nut13/nut13.zig"); +pub usingnamespace @import("nut12/nuts12.zig"); +pub usingnamespace @import("nut11/nut11.zig"); +pub usingnamespace @import("nut10/nut10.zig"); +pub usingnamespace @import("nut09/nut09.zig"); +pub const nut08 = @import("nut08/nut08.zig"); +pub usingnamespace @import("nut07/nut07.zig"); +pub usingnamespace @import("nut06/nut06.zig"); +pub const nut05 = @import("nut05/nut05.zig"); +pub const nut04 = @import("nut04/nut04.zig"); +pub usingnamespace @import("nut03/nut03.zig"); +pub usingnamespace @import("nut02/nut02.zig"); + +pub usingnamespace @import("nut01/nut01.zig"); +pub usingnamespace @import("nut00/lib.zig"); diff --git a/src/core/nuts/nut00/lib.zig b/src/core/nuts/nut00/lib.zig new file mode 100644 index 0000000..5cfbe5a --- /dev/null +++ b/src/core/nuts/nut00/lib.zig @@ -0,0 +1,536 @@ +const helper = @import("../../../helper/helper.zig"); +const secret = @import("../../secret.zig"); +const secp256k1 = @import("../../secp256k1.zig"); +const P2PKWitness = @import("../nut11/nut11.zig").P2PKWitness; +const HTLCWitness = @import("../nut14/nut14.zig").HTLCWitness; +const std = @import("std"); +const Id = @import("../nut02/nut02.zig").Id; +const BlindSignatureDleq = @import("../nut12/nuts12.zig").BlindSignatureDleq; +const ProofDleq = @import("../nut12/nuts12.zig").ProofDleq; +const amount_lib = @import("../../amount.zig"); +const dhke = @import("../../dhke.zig"); +const SpendingConditions = @import("../nut11/nut11.zig").SpendingConditions; +const Nut10Secret = @import("../nut10/nut10.zig").Secret; + +/// Proofs +pub const Proof = struct { + /// Amount + amount: u64, + /// `Keyset id` + keyset_id: Id, + /// Secret message + secret: secret.Secret, + // /// Unblinded signature + c: secp256k1.PublicKey, + // /// Witness + witness: ?Witness = null, + // /// DLEQ Proof + dleq: ?ProofDleq = null, + + pub usingnamespace helper.RenameJsonField( + @This(), + std.StaticStringMap([]const u8).initComptime( + &.{ + .{ + "c", "C", + }, + .{ + "keyset_id", "id", + }, + }, + ), + ); + + pub fn deinit(self: Proof, allocator: std.mem.Allocator) void { + if (self.witness) |w| w.deinit(); + self.secret.deinit(allocator); + } +}; + +/// Witness +pub const Witness = union(enum) { + /// P2PK Witness + p2pk_witness: P2PKWitness, + + /// HTLC Witness + htlc_witness: HTLCWitness, // TODO + + pub fn jsonParse(allocator: std.mem.Allocator, _source: anytype, options: std.json.ParseOptions) !@This() { + const parsed = try std.json.innerParse(std.json.Value, allocator, _source, options); + + var pss = std.json.Scanner.initCompleteInput(allocator, parsed.string); + defer pss.deinit(); + var source = &pss; + + var _signatures: ?helper.JsonArrayList([]const u8) = null; + var _preimage: ?[]const u8 = null; + + if (try source.next() != .object_begin) return error.UnexpectedToken; + + while (true) { + switch (try source.peekNextTokenType()) { + .string => { + const s = (try source.next()).string; + + if (std.mem.eql(u8, "signatures", s)) { + if (_signatures != null) return error.UnexpectedToken; + + _signatures = (try std.json.innerParse(helper.JsonArrayList([]const u8), allocator, source, options)); + continue; + } + + if (std.mem.eql(u8, "preimage", s)) { + if (_preimage != null) return error.UnexpectedToken; + + _preimage = switch (try source.next()) { + .string, .allocated_string => |ss| ss, + else => return error.UnexpectedToken, + }; + } + + return error.UnexpectedToken; + }, + .object_end => break, + else => return error.UnexpectedToken, + } + } + + const signs: ?std.ArrayList(std.ArrayList(u8)) = if (_signatures) |signs| try helper.clone2dSliceToArrayList(u8, allocator, signs.value.items) else null; + + if (_preimage != null) { + var pr = try std.ArrayList(u8).initCapacity(allocator, _preimage.?.len); + + pr.appendSliceAssumeCapacity(_preimage.?); + return .{ .htlc_witness = .{ + .preimage = pr, + .signatures = signs, + } }; + } + + // TODO better error + if (signs == null) return error.MissingField; + + return .{ + .p2pk_witness = .{ + .signatures = signs.?, + }, + }; + } + + pub fn deinit(self: Witness) void { + switch (self) { + .p2pk_witness => |w| w.deinit(), + .htlc_witness => |w| w.deinit(), + } + } + + /// Add signatures to [`Witness`] + pub fn addSignatures(self: *Witness, allocator: std.mem.Allocator, signs: []const []const u8) !void { + var clonedSignatures = try helper.clone2dSliceToArrayList(u8, allocator, signs); + defer clonedSignatures.deinit(); + errdefer { + for (clonedSignatures.items) |i| i.deinit(); + } + + switch (self.*) { + .p2pk_witness => |*p2pk_witness| try p2pk_witness.signatures.appendSlice(clonedSignatures.items), + .htlc_witness => |*htlc_witness| if (htlc_witness.signatures) |*_signs| try _signs.appendSlice(clonedSignatures.items), + } + } + + /// Get signatures on [`Witness`] + pub fn signatures(self: *const Witness) ?std.ArrayList(std.ArrayList(u8)) { + return switch (self.*) { + .p2pk_witness => |witness| witness.signatures, + .htlc_witness => |witness| witness.signatures, + }; + } + + /// Get preimage from [`Witness`] + pub fn preimage(self: *const Witness) ?[]const u8 { + return switch (self.*) { + .p2pk_witness => |_| null, + else => unreachable, + // Self::HTLCWitness(witness) => Some(witness.preimage.clone()), + }; + } +}; +/// Payment Method +pub const PaymentMethod = union(enum) { + /// Bolt11 payment type + bolt11, + /// Custom payment type: + custom: []const u8, + + pub fn fromString(method: []const u8) PaymentMethod { + if (std.mem.eql(u8, method, "bolt11")) return .bolt11; + + return .{ .custom = method }; + } + + pub fn toString(self: PaymentMethod) []const u8 { + return switch (self) { + .bolt11 => "bolt11", + .custom => |c| c, + }; + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + const value = try std.json.innerParse([]const u8, allocator, source, options); + + return PaymentMethod.fromString(value); + } + + pub fn jsonStringify(self: PaymentMethod, out: anytype) !void { + try out.write(self.toString()); + } +}; + +/// Currency Unit +pub const CurrencyUnit = enum { + /// Sat + sat, + /// Msat + msat, + /// Usd + usd, + /// Euro + eur, + + pub fn fromString(s: []const u8) !CurrencyUnit { + const kv = std.StaticStringMap(CurrencyUnit).initComptime(&.{ + .{ + "sat", .sat, + }, + .{ + "msat", .msat, + }, + .{ + "usd", .usd, + }, + .{ + "eur", .eur, + }, + }); + + return kv.get(s) orelse return error.UnsupportedUnit; + } + + pub fn toString(self: CurrencyUnit) []const u8 { + return switch (self) { + .sat => "sat", + .msat => "msat", + .usd => "usd", + .eur => "eur", + }; + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + const value = try std.json.innerParse([]const u8, allocator, source, options); + + return CurrencyUnit.fromString(value) catch return error.UnexpectedToken; + } + + pub fn jsonStringify(self: *const CurrencyUnit, out: anytype) !void { + try out.write(self.toString()); + } +}; + +/// Proof V4 +pub const ProofV4 = struct { + /// Amount in satoshi + // #[serde(rename = "a")] + amount: u64, + /// Secret message + // #[serde(rename = "s")] + secret: secret.Secret, + /// Unblinded signature + // #[serde( + // serialize_with = "serialize_v4_pubkey", + // deserialize_with = "deserialize_v4_pubkey" + // )] + // TODO ? different serializer + c: secp256k1.PublicKey, + /// Witness + witness: ?Witness, + /// DLEQ Proof + // #[serde(rename = "d")] + dleq: ?ProofDleq, +}; + +/// Blinded Message (also called `output`) +pub const BlindedMessage = struct { + /// Amount + /// + /// The value for the requested [BlindSignature] + amount: u64, + /// Keyset ID + /// + /// ID from which we expect a signature. + keyset_id: Id, + /// Blinded secret message (B_) + /// + /// The blinded secret message generated by the sender. + blinded_secret: secp256k1.PublicKey, + /// Witness + /// + /// + witness: ?Witness = null, + + pub usingnamespace helper.RenameJsonField( + @This(), + std.StaticStringMap([]const u8).initComptime( + &.{ + .{ + "blinded_secret", "B_", + }, + .{ + "keyset_id", "id", + }, + }, + ), + ); +}; + +/// Blind Signature (also called `promise`) +pub const BlindSignature = struct { + /// Amount + /// + /// The value of the blinded token. + amount: u64, + /// Keyset ID + /// + /// ID of the mint keys that signed the token. + keyset_id: Id, + /// Blinded signature (C_) + /// + /// The blinded signature on the secret message `B_` of [BlindedMessage]. + c: secp256k1.PublicKey, + /// DLEQ Proof + /// + /// + dleq: ?BlindSignatureDleq, + + pub usingnamespace helper.RenameJsonField( + @This(), + std.StaticStringMap([]const u8).initComptime( + &.{ + .{ + "c", "C_", + }, + .{ + "keyset_id", "id", + }, + }, + ), + ); +}; + +pub const PreMint = struct { + /// Blinded message + blinded_message: BlindedMessage, + /// Secret + secret: secret.Secret, + /// R + r: secp256k1.SecretKey, + /// Amount + amount: u64, + + // TODO implement methods +}; + +/// Premint Secrets +pub const PreMintSecrets = struct { + /// Secrets + secrets: []const PreMint, + /// Keyset Id + keyset_id: Id, + + // TODO implement methods + + /// Outputs for speceifed amount with random secret + pub fn random( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + keyset_id: Id, + amount: amount_lib.Amount, + amount_split_target: amount_lib.SplitTarget, + ) !helper.Parsed(PreMintSecrets) { + var pre_mint_secrets = try helper.Parsed(PreMintSecrets).init(allocator); + errdefer pre_mint_secrets.deinit(); + + const amount_split = try amount_lib.splitTargeted(amount, allocator, amount_split_target); + defer amount_split.deinit(); + + var output = try std.ArrayList(PreMint).initCapacity(pre_mint_secrets.arena.allocator(), amount_split.items.len); + defer output.deinit(); + + for (amount_split.items) |amnt| { + const sec = try secret.Secret.generate(pre_mint_secrets.arena.allocator()); + + const blinded, const r = try dhke.blindMessage(secp, sec.toBytes(), null); + const blinded_message = BlindedMessage{ + .amount = amnt, + .keyset_id = keyset_id, + .blinded_secret = blinded, + }; + + try output.append(.{ + .secret = sec, + .blinded_message = blinded_message, + .r = r, + .amount = amnt, + }); + } + + pre_mint_secrets.value.secrets = try output.toOwnedSlice(); + + return pre_mint_secrets; + } + + /// Outputs from pre defined secrets + pub fn fromSecrets( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + keyset_id: Id, + amounts: []const amount_lib.Amount, + secrets: []const secret.Secret, + ) !helper.Parsed(PreMintSecrets) { + var pre_mint_secrets = try helper.Parsed(PreMintSecrets).init(allocator); + errdefer pre_mint_secrets.deinit(); + + var output = try std.ArrayList(PreMint).initCapacity(pre_mint_secrets.arena.allocator(), secrets.len); + defer output.deinit(); + + for (secrets, amounts) |sec, amount| { + const blinded, const r = try dhke.blindMessage(secp, sec.toBytes(), null); + + const blinded_message = BlindedMessage{ + .amount = amount, + .keyset_id = keyset_id, + .blinded_secret = blinded, + }; + + try output.append(.{ + .secret = try sec.clone(pre_mint_secrets.arena.allocator()), + .blinded_message = blinded_message, + .r = r, + .amount = amount, + }); + } + + pre_mint_secrets.value.secrets = try output.toOwnedSlice(); + return pre_mint_secrets; + } + + /// Blank Outputs used for NUT-08 change + pub fn blank( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + keyset_id: Id, + fee_reserve: amount_lib.Amount, + ) !helper.Parsed(PreMintSecrets) { + var pre_mint_secrets = try helper.Parsed(PreMintSecrets).init(allocator); + errdefer pre_mint_secrets.deinit(); + + const count = @max(1, @as(u64, @intFromFloat(std.math.ceil(std.math.log2(@as(f64, @floatFromInt(fee_reserve))))))); + + var output = try std.ArrayList(PreMint).initCapacity(pre_mint_secrets.arena.allocator(), count); + defer output.deinit(); + + for (0..count) |_| { + const sec = try secret.Secret.generate(pre_mint_secrets.arena.allocator()); + const blinded, const r = try dhke.blindMessage(secp, sec.toBytes(), null); + + const blinded_message = BlindedMessage{ + .amount = 0, + .keyset_id = keyset_id, + .blinded_secret = blinded, + }; + + try output.append(.{ + .secret = sec, + .blinded_message = blinded_message, + .r = r, + .amount = 0, + }); + } + + pre_mint_secrets.value.secrets = try output.toOwnedSlice(); + return pre_mint_secrets; + } + + // /// Outputs with specific spending conditions + pub fn withConditions( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + keyset_id: Id, + amount: amount_lib.Amount, + amount_split_target: amount_lib.SplitTarget, + conditions: SpendingConditions, + ) !helper.Parsed(PreMintSecrets) { + var pre_mint_secrets = try helper.Parsed(PreMintSecrets).init(allocator); + errdefer pre_mint_secrets.deinit(); + + const amount_split = try amount_lib.splitTargeted(amount, allocator, amount_split_target); + defer amount_split.deinit(); + + var output = try std.ArrayList(PreMint).initCapacity(pre_mint_secrets.arena.allocator(), amount_split.items.len); + defer output.deinit(); + + for (amount_split.items) |amnt| { + var sec10 = try conditions.toSecret(allocator); + defer sec10.deinit(); + + const sec = try sec10.toSecret(pre_mint_secrets.arena.allocator()); + + const blinded, const r = try dhke.blindMessage(secp, sec.toBytes(), null); + + const blinded_message = BlindedMessage{ + .amount = amnt, + .keyset_id = keyset_id, + .blinded_secret = blinded, + }; + + try output.append(.{ + .secret = sec, + .blinded_message = blinded_message, + .r = r, + .amount = amnt, + }); + } + + pre_mint_secrets.value.secrets = try output.toOwnedSlice(); + return pre_mint_secrets; + } +}; + +test "test_proof_serialize" { + const proof = + "[{\"id\":\"009a1f293253e41e\",\"amount\":2,\"secret\":\"407915bc212be61a77e3e6d2aeb4c727980bda51cd06a6afc29e2861768a7837\",\"C\":\"02bc9097997d81afb2cc7346b5e4345a9346bd2a506eb7958598a72f0cf85163ea\"},{\"id\":\"009a1f293253e41e\",\"amount\":8,\"secret\":\"fe15109314e61d7756b0f8ee0f23a624acaa3f4e042f61433c728c7057b931be\",\"C\":\"029e8e5050b890a7d6c0968db16bc1d5d5fa040ea1de284f6ec69d61299f671059\"}]"; + + const proofs = try std.json.parseFromSlice([]const Proof, std.testing.allocator, proof, .{}); + defer proofs.deinit(); + + try std.testing.expectEqualDeep(try Id.fromStr("009a1f293253e41e"), proofs.value[0].keyset_id); + try std.testing.expectEqualDeep(proofs.value.len, 2); +} + +test "test_blank_blinded_messages" { + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + { + const b = try PreMintSecrets.blank(std.testing.allocator, secp, try Id.fromStr("009a1f293253e41e"), 1000); + defer b.deinit(); + + try std.testing.expectEqual(10, b.value.secrets.len); + } + + { + const b = try PreMintSecrets.blank(std.testing.allocator, secp, try Id.fromStr("009a1f293253e41e"), 1); + defer b.deinit(); + + try std.testing.expectEqual(1, b.value.secrets.len); + } +} diff --git a/src/core/nuts/nut00/token.zig b/src/core/nuts/nut00/token.zig new file mode 100644 index 0000000..0a2f16c --- /dev/null +++ b/src/core/nuts/nut00/token.zig @@ -0,0 +1,58 @@ +//! Cashu Token +//! +//! +const std = @import("std"); +const CurrencyUnit = @import("lib.zig").CurrencyUnit; +const Proof = @import("lib.zig").Proof; +const helper = @import("../../../helper/helper.zig"); +const Id = @import("../nut02/nut02.zig").Id; + +/// Token +pub const TokenV3 = struct { + /// Proofs in [`Token`] by mint + token: []const TokenV3Token, + /// Memo for token + memo: ?[]const u8, + /// Token Unit + unit: ?CurrencyUnit, +}; + +/// Token V3 Token +pub const TokenV3Token = struct { + /// Url of mint + mint: []const u8, + /// [`Proofs`] + proofs: []const Proof, +}; + +/// Token V4 +pub const TokenV4 = struct { + /// Mint Url + // #[serde(rename = "m")] + mint_url: []const u8, + /// Token Unit + // #[serde(rename = "u", skip_serializing_if = "Option::is_none")] + unit: ?CurrencyUnit, + /// Memo for token + // #[serde(rename = "d", skip_serializing_if = "Option::is_none")] + memo: ?[]const u8, + /// Proofs + /// + /// Proofs separated by keyset_id + // #[serde(rename = "t")] + token: []const TokenV4Token, +}; + +/// Token V4 Token +pub const TokenV4Token = struct { + /// `Keyset id` + // #[serde( + // rename = "i", + // serialize_with = "serialize_v4_keyset_id", + // deserialize_with = "deserialize_v4_keyset_id" + // )] + keyset_id: Id, + /// Proofs + // #[serde(rename = "p")] + proofs: []const ProofV4, +}; diff --git a/src/core/nuts/nut01/nut01.zig b/src/core/nuts/nut01/nut01.zig new file mode 100644 index 0000000..c0126c9 --- /dev/null +++ b/src/core/nuts/nut01/nut01.zig @@ -0,0 +1,185 @@ +const std = @import("std"); +const secp256k1 = @import("../../secp256k1.zig"); +const KeySet = @import("../nut02/nut02.zig").KeySet; + +/// Mint Public Keys [NUT-01] +pub const KeysResponse = struct { + /// Keysets + keysets: []const KeySet, + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !KeysResponse { + if (try source.next() != .object_begin) return error.UnexpectedToken; + + const tokenType = switch (try source.next()) { + .string, .allocated_string => |s| s, + else => return error.UnexpectedToken, + }; + + if (!std.mem.eql(u8, tokenType, "keysets")) return error.MissingField; + + if (try source.next() != .array_begin) return error.UnexpectedToken; + + var arraylist = std.ArrayList(KeySet).init(allocator); + while (true) { + switch (try source.peekNextTokenType()) { + .array_end => { + _ = try source.next(); + break; + }, + else => {}, + } + + try arraylist.ensureUnusedCapacity(1); + + arraylist.appendAssumeCapacity(std.json.innerParse(KeySet, allocator, source, options) catch |e| if (e == std.mem.Allocator.Error.OutOfMemory) return e else { + continue; + }); + } + + if (try source.next() != .object_end) return error.UnexpectedToken; + + return .{ + .keysets = try arraylist.toOwnedSlice(), + }; + } +}; + +/// Mint Keys [NUT-01] +pub const Keys = struct { + inner: std.StringHashMap(secp256k1.PublicKey), + + /// Get [`PublicKey`] for [`Amount`] + pub inline fn amountKey(self: *const Keys, amount: u64) ?secp256k1.PublicKey { + var buf: [21]u8 = undefined; + + const n = std.fmt.formatIntBuf(&buf, amount, 10, .lower, .{}); + + return self.inner.get(buf[0..n]); + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !Keys { + if (try source.next() != .object_begin) return error.UnexpectedToken; + + var result = std.StringHashMap(secp256k1.PublicKey).init(allocator); + errdefer result.deinit(); + + while (try source.peekNextTokenType() != .object_end) { + switch (try source.next()) { + .string, .allocated_string => |s| { + const val = try std.json.innerParse(secp256k1.PublicKey, allocator, source, options); + + try result.put(s, val); + }, + else => return error.UnexpectedToken, + } + } + + // array_end + _ = try source.next(); + return .{ .inner = result }; + } + + pub fn deinit(self: *Keys) void { + self.inner.deinit(); + } + + pub fn fromMintKeys(allocator: std.mem.Allocator, keys: MintKeys) !Keys { + var res = std.StringHashMap(secp256k1.PublicKey).init(allocator); + errdefer res.deinit(); + + var buf: [20]u8 = undefined; + + var it = keys.inner.iterator(); + while (it.next()) |e| { + const sz = std.fmt.formatIntBuf(&buf, e.key_ptr.*, 10, .lower, .{}); + + try res.put(buf[0..sz], e.value_ptr.public_key); + } + + return .{ .inner = res }; + } +}; + +/// Mint keys +pub const MintKeys = struct { + inner: std.AutoHashMap( + u64, + MintKeyPair, + ), + + /// Create new [`MintKeys`] + pub inline fn initFrom(allocator: std.mem.Allocator, map: std.AutoHashMap(u64, MintKeyPair)) !MintKeys { + return .{ + .inner = try map.cloneWithAllocator(allocator), + }; + } +}; + +/// Mint Public Private key pair +pub const MintKeyPair = struct { + /// Publickey + public_key: secp256k1.PublicKey, + /// Secretkey + secret_key: secp256k1.SecretKey, + + /// [`MintKeyPair`] from secret key + pub inline fn fromSecretKey(secp: secp256k1.Secp256k1, secret_key: secp256k1.SecretKey) MintKeyPair { + return .{ + .public_key = secret_key.publicKey(secp), + .secret_key = secret_key, + }; + } +}; + +test "pubkey" { + const pubkey_str = "02c020067db727d586bc3183aecf97fcb800c3f4cc4759f69c626c9db5d8f5b5d4"; + const pubkey = try secp256k1.PublicKey.fromString(pubkey_str); + + try std.testing.expectEqualSlices(u8, pubkey_str, &pubkey.toString()); +} + +test "test_ser_der_secret" { + const secret = secp256k1.SecretKey.generate(); + + var output = std.ArrayList(u8).init(std.testing.allocator); + defer output.deinit(); + + try std.json.stringify(&secret, .{}, output.writer()); + + const sec = try std.json.parseFromSlice(secp256k1.SecretKey, std.testing.allocator, output.items, .{}); + defer sec.deinit(); + + try std.testing.expectEqual(secret, sec.value); +} + +test "test_vectors_01" { + const incorrect_1 = + \\{ + \\"1":"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38","2":"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de","4":"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303","8":"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528" + \\} + ; + try std.testing.expect(if (std.json.parseFromSlice(Keys, std.testing.allocator, incorrect_1, .{})) |_| false else |_| true); + + const incorrect_2 = + \\{ + \\"1":"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc","2":"04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481","4":"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303","8":"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528" + \\} + ; + try std.testing.expect(if (std.json.parseFromSlice(Keys, std.testing.allocator, incorrect_2, .{})) |_| false else |_| true); + + const correct_1 = + \\{ + \\ "1":"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc","2":"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de","4":"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303","8":"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528" + \\} + ; + var val = try std.json.parseFromSlice(Keys, std.testing.allocator, correct_1, .{}); + defer val.deinit(); + + const correct_2 = + \\{ + \\"1":"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566","2":"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5","4":"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7","8":"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0","16":"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d","32":"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612","64":"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664","128":"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9","256":"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459","512":"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb","1024":"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc","2048":"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b","4096":"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2","8192":"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21","16384":"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50","32768":"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04","65536":"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d","131072":"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41","262144":"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328","524288":"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86","1048576":"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788","2097152":"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c","4194304":"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512","8388608":"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0","16777216":"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21","33554432":"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262","67108864":"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3","134217728":"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020","268435456":"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276","536870912":"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9","1073741824":"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee","2147483648":"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a","4294967296":"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5","8589934592":"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3","17179869184":"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9","34359738368":"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75","68719476736":"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754","137438953472":"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6","274877906944":"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a","549755813888":"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785","1099511627776":"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a","2199023255552":"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258","4398046511104":"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a","8796093022208":"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e","17592186044416":"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310","35184372088832":"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06","70368744177664":"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1","140737488355328":"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed","281474976710656":"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d","562949953421312":"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a","1125899906842624":"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9","2251799813685248":"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f","4503599627370496":"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73","9007199254740992":"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49","18014398509481984":"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6","36028797018963968":"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0","72057594037927936":"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd","144115188075855872":"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a","288230376151711744":"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc","576460752303423488":"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a","1152921504606846976":"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06","2305843009213693952":"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099","4611686018427387904":"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f","9223372036854775808":"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad" + \\} + ; + var val1 = try std.json.parseFromSlice(Keys, std.testing.allocator, correct_2, .{}); + defer val1.deinit(); +} diff --git a/src/core/nuts/nut02/nut02.zig b/src/core/nuts/nut02/nut02.zig new file mode 100644 index 0000000..c29fe9e --- /dev/null +++ b/src/core/nuts/nut02/nut02.zig @@ -0,0 +1,297 @@ +//! NUT-02: Keysets and keyset ID +//! +//! + +const std = @import("std"); +const CurrencyUnit = @import("../nut00/lib.zig").CurrencyUnit; +const Keys = @import("../nut01/nut01.zig").Keys; +const MintKeys = @import("../nut01/nut01.zig").MintKeys; +const MintKeyPair = @import("../nut01/nut01.zig").MintKeyPair; +const secp256k1 = @import("../../secp256k1.zig"); +const bip32 = @import("../../bip32/bip32.zig"); + +/// Keyset version +pub const KeySetVersion = enum { + /// Current Version 00 + version00, + + /// [`KeySetVersion`] to byte + pub fn toByte(self: KeySetVersion) u8 { + return switch (self) { + .version00 => 0, + }; + } + + /// [`KeySetVersion`] from byte + pub fn fromByte(byte: u8) !KeySetVersion { + return switch (byte) { + 0 => KeySetVersion.version00, + else => error.UnknownVersion, + }; + } +}; + +const STRLEN = 14; +const BYTELEN = 7; + +// A keyset ID is an identifier for a specific keyset. It can be derived by +// Anyone who knows the set of public keys of a mint. The keyset ID **CAN** +// be stored in a Cashu token such that the token can be used to identify +// which mint or keyset it was generated from. +pub const Id = struct { + version: KeySetVersion, + id: [BYTELEN]u8, + + pub fn toBytes(self: Id) [BYTELEN + 1]u8 { + return [_]u8{self.version.toByte()} ++ self.id; + } + + pub fn toU64(self: Id) !u64 { + const hex_bytes: [8]u8 = self.toBytes(); + + const int = std.mem.readInt(u64, &hex_bytes, .big); + + return int % comptime ((std.math.powi(u64, 2, 31) catch unreachable) - 1); + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + const str = try std.json.innerParse([]const u8, allocator, source, options); + + return Id.fromStr(str) catch return error.UnexpectedToken; + } + + pub fn jsonStringify(self: *const Id, out: anytype) !void { + // TODO use version + try out.write(std.fmt.bytesToHex(self.toBytes(), .lower)); + } + + pub fn fromStr(s: []const u8) !Id { + // Check if the string length is valid + if (s.len != 16) { + return error.Length; + } + var ret = Id{ + .version = .version00, + .id = undefined, + }; + + // should we check return size of hex to bytes? + _ = try std.fmt.hexToBytes(&ret.id, s[2..]); + + return ret; + } + + const st = struct { u64, secp256k1.PublicKey }; + + fn compare(_: void, lhs: st, rhs: st) bool { + return lhs[0] < rhs[0]; + } + + pub fn fromKeys(allocator: std.mem.Allocator, map: std.StringHashMap(secp256k1.PublicKey)) !Id { + // REVIEW: Is it 16 or 14 bytes + // NUT-02 + // 1 - sort public keys by their amount in ascending order + // 2 - concatenate all public keys to one string + // 3 - HASH_SHA256 the concatenated public keys + // 4 - take the first 14 characters of the hex-encoded hash + // 5 - prefix it with a keyset ID version byte + var it = map.iterator(); + + var arr = try std.ArrayList(st).initCapacity(allocator, map.count()); + defer arr.deinit(); + + while (it.next()) |v| { + const num = try std.fmt.parseInt(u64, v.key_ptr.*, 10); + arr.appendAssumeCapacity(.{ num, v.value_ptr.* }); + } + + std.sort.block(st, arr.items, {}, compare); + + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + for (arr.items) |pk| hasher.update(&pk[1].serialize()); + + const hash = hasher.finalResult(); + const hex_of_hash = std.fmt.bytesToHex(hash, .lower); + + var buf: [7]u8 = undefined; + + _ = try std.fmt.hexToBytes(&buf, hex_of_hash[0..14]); + + return .{ + .version = .version00, + .id = buf, + }; + } + + pub fn fromMintKeys(allocator: std.mem.Allocator, mkeys: MintKeys) !Id { + var keys = try Keys.fromMintKeys(allocator, mkeys); + defer keys.deinit(); + + return try fromKeys(allocator, keys.inner); + } +}; + +/// Keyset +pub const KeySet = struct { + /// Keyset [`Id`] + id: Id, + /// Keyset [`CurrencyUnit`] + unit: CurrencyUnit, + /// Keyset [`Keys`] + keys: Keys, +}; + +/// KeySetInfo +pub const KeySetInfo = struct { + /// Keyset [`Id`] + id: Id, + /// Keyset [`CurrencyUnit`] + unit: CurrencyUnit, + /// Keyset state + /// Mint will only sign from an active keyset + active: bool, + /// Input Fee PPK + input_fee_ppk: u64 = 0, +}; + +/// MintKeyset +pub const MintKeySet = struct { + /// Keyset [`Id`] + id: Id, + /// Keyset [`CurrencyUnit`] + unit: CurrencyUnit, + /// Keyset [`MintKeys`] + keys: MintKeys, + + pub fn toKeySet(self: MintKeySet, arena: std.mem.Allocator) !KeySet { + return .{ + .id = self.id, + .unit = self.unit, + .keys = try Keys.fromMintKeys(arena, self.keys), + }; + } + + /// Generate new [`MintKeySet`] + pub fn generate( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + xpriv: bip32.ExtendedPrivKey, + unit: CurrencyUnit, + max_order: u8, + ) !MintKeySet { + var map = std.AutoHashMap(u64, MintKeyPair).init(allocator); + errdefer map.deinit(); + for (0..max_order) |i| { + const amount = try std.math.powi(u64, 2, i); + + const secret_key = (try xpriv.derivePriv( + secp, + &.{try bip32.ChildNumber.fromHardenedIdx(@intCast(i))}, + )).private_key; + + const public_key = secret_key.publicKey(secp); + try map.put(amount, .{ + .secret_key = secret_key, + .public_key = public_key, + }); + } + + const keys = MintKeys{ + .inner = map, + }; + + return .{ + .id = try Id.fromMintKeys(allocator, keys), + .unit = unit, + .keys = keys, + }; + } + + /// Generate new [`MintKeySet`] from seed + pub fn generateFromSeed( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + seed: []const u8, + max_order: u8, + currency_unit: CurrencyUnit, + derivation_path: []const bip32.ChildNumber, + ) !MintKeySet { + const xpriv = + bip32.ExtendedPrivKey.initMaster(.MAINNET, seed) catch @panic("RNG busted"); + + return try generate( + allocator, + secp, + try xpriv + .derivePriv(secp, derivation_path), + currency_unit, + max_order, + ); + } + + /// Generate new [`MintKeySet`] from xpriv + pub fn generateFromXpriv( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + xpriv: bip32.ExtendedPrivKey, + max_order: u8, + currency_unit: CurrencyUnit, + derivation_path: []const bip32.ChildNumber, + ) !MintKeySet { + return try generate( + allocator, + secp, + xpriv + .derivePriv(secp, derivation_path) catch @panic("RNG busted"), + currency_unit, + max_order, + ); + } +}; + +/// Mint Keysets [NUT-02] +/// Ids of mints keyset ids +pub const KeysetResponse = struct { + /// set of public key ids that the mint generates + keysets: []const KeySetInfo, + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !KeysetResponse { + if (try source.next() != .object_begin) return error.UnexpectedToken; + + const tokenType = switch (try source.next()) { + .string, .allocated_string => |s| s, + else => return error.UnexpectedToken, + }; + + if (!std.mem.eql(u8, tokenType, "keysets")) return error.MissingField; + + if (try source.next() != .array_begin) return error.UnexpectedToken; + + var arraylist = std.ArrayList(KeySetInfo).init(allocator); + while (true) { + switch (try source.peekNextTokenType()) { + .array_end => { + _ = try source.next(); + break; + }, + else => {}, + } + + try arraylist.ensureUnusedCapacity(1); + + arraylist.appendAssumeCapacity(std.json.innerParse(KeySetInfo, allocator, source, options) catch |e| if (e == std.mem.Allocator.Error.OutOfMemory) return e else { + continue; + }); + } + + if (try source.next() != .object_end) return error.UnexpectedToken; + + return .{ + .keysets = try arraylist.toOwnedSlice(), + }; + } +}; + +test { + _ = @import("nut02_test.zig"); +} diff --git a/src/core/nuts/nut02/nut02_test.zig b/src/core/nuts/nut02/nut02_test.zig new file mode 100644 index 0000000..26264aa --- /dev/null +++ b/src/core/nuts/nut02/nut02_test.zig @@ -0,0 +1,156 @@ +const nut02 = @import("nut02.zig"); +const nut01 = @import("../nut01/nut01.zig"); +const Id = nut02.Id; +const Keys = nut01.Keys; +const std = @import("std"); + +const SHORT_KEYSET_ID = "00456a94ab4e1c46"; +const SHORT_KEYSET = + \\{ + \\ "1":"03a40f20667ed53513075dc51e715ff2046cad64eb68960632269ba7f0210e38bc", + \\ "2":"03fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de", + \\ "4":"02648eccfa4c026960966276fa5a4cae46ce0fd432211a4f449bf84f13aa5f8303", + \\ "8":"02fdfd6796bfeac490cbee12f778f867f0a2c68f6508d17c649759ea0dc3547528" + \\} +; +const KEYSET_ID = "000f01df73ea149a"; +const KEYSET = + \\{ + \\ "1":"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566", + \\ "2":"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5", + \\ "4":"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7", + \\ "8":"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0", + \\ "16":"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d", + \\ "32":"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612", + \\ "64":"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664", + \\ "128":"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9", + \\ "256":"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459", + \\ "512":"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb", + \\ "1024":"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc", + \\ "2048":"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b", + \\ "4096":"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2", + \\ "8192":"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21", + \\ "16384":"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50", + \\ "32768":"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04", + \\ "65536":"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d", + \\ "131072":"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41", + \\ "262144":"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328", + \\ "524288":"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86", + \\ "1048576":"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788", + \\ "2097152":"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c", + \\ "4194304":"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512", + \\ "8388608":"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0", + \\ "16777216":"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21", + \\ "33554432":"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262", + \\ "67108864":"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3", + \\ "134217728":"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020", + \\ "268435456":"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276", + \\ "536870912":"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9", + \\ "1073741824":"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee", + \\ "2147483648":"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a", + \\ "4294967296":"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5", + \\ "8589934592":"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3", + \\ "17179869184":"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9", + \\ "34359738368":"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75", + \\ "68719476736":"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754", + \\ "137438953472":"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6", + \\ "274877906944":"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a", + \\ "549755813888":"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785", + \\ "1099511627776":"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a", + \\ "2199023255552":"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258", + \\ "4398046511104":"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a", + \\ "8796093022208":"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e", + \\ "17592186044416":"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310", + \\ "35184372088832":"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06", + \\ "70368744177664":"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1", + \\ "140737488355328":"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed", + \\ "281474976710656":"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d", + \\ "562949953421312":"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a", + \\ "1125899906842624":"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9", + \\ "2251799813685248":"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f", + \\ "4503599627370496":"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73", + \\ "9007199254740992":"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49", + \\ "18014398509481984":"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6", + \\ "36028797018963968":"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0", + \\ "72057594037927936":"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd", + \\ "144115188075855872":"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a", + \\ "288230376151711744":"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc", + \\ "576460752303423488":"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a", + \\ "1152921504606846976":"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06", + \\ "2305843009213693952":"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099", + \\ "4611686018427387904":"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f", + \\ "9223372036854775808":"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad" + \\} +; + +test "test_deserialization_and_id_generation" { + const _id = try Id.fromStr("009a1f293253e41e"); + _ = _id; // autofix + + const keys = try std.json.parseFromSlice(Keys, std.testing.allocator, SHORT_KEYSET, .{}); + defer keys.deinit(); + + const id = try Id.fromKeys(std.testing.allocator, keys.value.inner); + + try std.testing.expectEqual(try Id.fromStr(SHORT_KEYSET_ID), id); + + const keys1 = try std.json.parseFromSlice(Keys, std.testing.allocator, KEYSET, .{}); + defer keys1.deinit(); + + const id1 = try Id.fromKeys(std.testing.allocator, keys1.value.inner); + + try std.testing.expectEqual(try Id.fromStr(KEYSET_ID), id1); +} + +test "test_deserialization_keyset_info" { + const h = + \\{"id":"009a1f293253e41e","unit":"sat","active":true} + ; + + const _keyset_response = try std.json.parseFromSlice(nut02.KeySetInfo, std.testing.allocator, h, .{}); + defer _keyset_response.deinit(); + + const encoded = try std.json.stringifyAlloc(std.testing.allocator, &_keyset_response.value, .{}); + defer std.testing.allocator.free(encoded); + + const decoded = try std.json.parseFromSlice(nut02.KeySetInfo, std.testing.allocator, encoded, .{}); + defer decoded.deinit(); + + try std.testing.expectEqualDeep(_keyset_response.value, decoded.value); +} + +test "test_deserialization_of_keyset_response" { + const h = + \\{"keysets":[{"id":"009a1f293253e41e","unit":"sat","active":true, "input_fee_ppk": 100},{"id":"eGnEWtdJ0PIM","unit":"sat","active":true},{"id":"003dfdf4e5e35487","unit":"sat","active":true},{"id":"0066ad1a4b6fc57c","unit":"sat","active":true},{"id":"00f7ca24d44c3e5e","unit":"sat","active":true},{"id":"001fcea2931f2d85","unit":"sat","active":true},{"id":"00d095959d940edb","unit":"sat","active":true},{"id":"000d7f730d657125","unit":"sat","active":true},{"id":"0007208d861d7295","unit":"sat","active":true},{"id":"00bfdf8889b719dd","unit":"sat","active":true},{"id":"00ca9b17da045f21","unit":"sat","active":true}]} + ; + + const response = try std.json.parseFromSlice(nut02.KeysetResponse, std.testing.allocator, h, .{}); + defer response.deinit(); +} + +test "test_to_int" { + const id = try Id.fromStr("009a1f293253e41e"); + + const id_int = try id.toU64(); + + try std.testing.expectEqual(864559728, id_int); +} + +test "test_keyset_bytes" { + const id = try Id.fromStr("009a1f293253e41e"); + + const id_bytes = id.toBytes(); + + try std.testing.expectEqual(8, id_bytes.len); +} + +test "dfd" { + const json = + \\{"keysets":[{"id":"I2yN+iRYfkzT","unit":"sat","keys":{"1":"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566","2":"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5","4":"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7","8":"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0","16":"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d","32":"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612","64":"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664","128":"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9","256":"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459","512":"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb","1024":"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc","2048":"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b","4096":"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2","8192":"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21","16384":"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50","32768":"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04","65536":"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d","131072":"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41","262144":"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328","524288":"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86","1048576":"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788","2097152":"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c","4194304":"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512","8388608":"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0","16777216":"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21","33554432":"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262","67108864":"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3","134217728":"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020","268435456":"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276","536870912":"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9","1073741824":"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee","2147483648":"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a","4294967296":"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5","8589934592":"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3","17179869184":"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9","34359738368":"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75","68719476736":"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754","137438953472":"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6","274877906944":"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a","549755813888":"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785","1099511627776":"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a","2199023255552":"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258","4398046511104":"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a","8796093022208":"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e","17592186044416":"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310","35184372088832":"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06","70368744177664":"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1","140737488355328":"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed","281474976710656":"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d","562949953421312":"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a","1125899906842624":"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9","2251799813685248":"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f","4503599627370496":"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73","9007199254740992":"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49","18014398509481984":"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6","36028797018963968":"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0","72057594037927936":"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd","144115188075855872":"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a","288230376151711744":"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc","576460752303423488":"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a","1152921504606846976":"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06","2305843009213693952":"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099","4611686018427387904":"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f","9223372036854775808":"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad"}},{"id":"00759e3f8b06b36f","unit":"sat","keys":{"1":"038a935c51c76c780ff9731cfbe9ab477f38346775809fa4c514340feabbec4b3a","2":"038288b12ebf2db3645e5d58835bd100398b6b19dfef338c698b55c05d0d41fb0a","4":"02fc8201cf4ea29abac0495d1304064f0e698762b8c0db145c1737b38a9d61c7e2","8":"02274243e03ca19f969acc7072812405b38adc672d1d753e65c63746b3f31cc6eb","16":"025f07cb2493351e7d5202f05eaf3934d5c9d17e73385e9de5bfab802f7d8caf92","32":"03afce0a897c858d7c88c1454d492eac43011e3396dda5b778ba1fcab381c748b1","64":"037b2178f42507f0c95e09d9b435a127df4b3e23ccd20af8075817d3abe90947ad","128":"02ebce8457b48407d4d248dba5a31b3eabf08a6285d09d08e40681c4adaf77bd40","256":"03c89713d27d6f8e328597b43dd87623efdcb251a484932f9e095ebfb6dbf4bdf2","512":"02df10f3ebba69916d03ab1754488770498f2e5466224d6df6d12811a13e46776c","1024":"02f5d9cba0502c21c6b39938a09dcb0390f124a2fd65e45dfeccd153cc1864273d","2048":"039de1dad91761b194e7674fb6ba212241aaf7f49dcb578a8fe093196ad1b20d1c","4096":"03cc694ba22e455f1c22b2cee4a40ecdd4f3bb4da0745411adb456158372d3efbb","8192":"029d66c24450fc315e046010df6870d61daa90c5c486c5ec4d7d3b99c5c2bce923","16384":"0387d063821010c7bd5cf79441870182f70cd432d13d3fc255e7b6ffd82c9d3c5a","32768":"021a94c6c03f7de8feb25b8a8b8d1f1c6f56af4bc533eb97c9e8b89c76b616ff11","65536":"038989c6ed91a7c577953115b465ee400a270a64e95eda8f7ee9d6bf30b8fe4908","131072":"03c3d3cd2523f004ee479a170b0ec5c74c060edb8356fc1b0a9ed8087cf6345172","262144":"02e54a7546f1a9194f30baa593a13d4e2949eb866593445d89675d7d394ef6320b","524288":"034e91037b3f1d3258d1e871dede80e98ef83e307c2e5ff589f38bd046f97546f8","1048576":"03306d42752a1adcfa394af2a690961ac9b80b1ac0f5fdc0890f66f8dc7d25ac6e","2097152":"03ec114332fe798c3e36675566c4748fda7d881000a01864ec48486512d7901e76","4194304":"02095e3e443d98ca3dfabcebc2f9154f3656b889783f7edb8290cfb01f497e63cf","8388608":"03c90f31525a4f9ab6562ec3edbf2bafc6662256ea6ce82ab19a45d2aee80b2f15","16777216":"03c0ae897a45724465c713c1379671ac5ff0a81c32e5f2dd27ea7e5530c7af484c","33554432":"034bcf793b70ba511e9c84cd07fc0c73c061e912bc02df4cac7871d048bad653b6","67108864":"021c6826c23a181d14962f43121943569a54f9d5af556eb839aee42d3f62debee6","134217728":"030e1bc651b6496922978d6cd3ed923cbf12b4332c496f841f506f5abf9d186d35","268435456":"03e3219e50cf389a75794f82ab4f880f5ffe9ca227b992c3e93cb4bb659d8e3353","536870912":"03879ad42536c410511ac6956b9da2d0da59ce7fbb6068bd9b25dd7cccddcc8096","1073741824":"03c4d3755a17904c0cfa7d7a21cc5b4e85fca8ac85369fcb12a6e2177525117dee","2147483648":"02e7a5d5cd3ea24f05f741dddad3dc8c5e24db60eb9bf9ad888b1c5dfbd792665e","4294967296":"03c783d24d8c9e51207eb3d6199bf48d6eb81a4b34103b422724be15501ff921bd","8589934592":"03200234495725455f4c4e6b6cb7b7936eb7cd1d1c9bb73d2ce032bae7d728b3ca","17179869184":"02eafa50ac67de2c206d1a67245b72ec20fac081c2a550294cc0a711246ed65a41","34359738368":"024c153c2a56de05860006aff9dc35ec9cafd7ac68708442a3a326c858b0c1a146","68719476736":"035a890c2d5c8bf259b98ac67d0d813b87778bcb0c0ea1ee9717ac804b0be3f563","137438953472":"025184ca832f08b105fdb471e2caf14025a1daa6f44ce90b4c7703878ccb6b26e8","274877906944":"039d19a41abdd49949c60672430018c63f27c5a28991f9fbb760499daccc63146c","549755813888":"03a138ac626dd3e6753459903aa128a13c052ed0058f2ead707c203bd4a7565237","1099511627776":"0298c8ef2eab728613103481167102efaf2d4b7a303cb94b9393da37a034a95c53","2199023255552":"02d88f8fc93cd2edf303fdebfecb70e59b5373cb8f746a1d075a9c86bc9382ac07","4398046511104":"02afd89ee23eee7d5fe6687fee898f64e9b01913ec71b5c596762b215e040c701f","8796093022208":"02196b461f3c804259e597c50e514920427aab4beaef0c666185fb2ff4399813db","17592186044416":"037b33746a6fd7a71d4cf17c85d13a64b98620614c0028d4995163f1b8484ee337","35184372088832":"036cce0a1878bbc63b3108c379ef4e6529fbf20ed675d80d91ca3ccc55fde4bdbd","70368744177664":"039c81dccb319ba70597cdf9db33b459164a1515c27366c8f667b01d988874e554","140737488355328":"036b2dd85a3c44c4458f0b246ce19a1524a191f1716834cfb452c6e1f946172c19","281474976710656":"022c84722c31a2b3d8cfd9b6a9e6199515fd97d6a9c390fc3d82f123bfc501ad04","562949953421312":"0355e2be85ee599b8fa7e6e68a9954573d032e89aa9e65c2e1231991664c200bf3","1125899906842624":"024b10818cd27f3eec6c9daf82b9dfa53928ab0711b711070bd39892ac10dee765","2251799813685248":"02a6d726432bb18c3145eba4fc0b587bf64f3be8617c0070dda33944474b3f8740","4503599627370496":"0248304be3cbaf31ec320bc636bb936c5984caf773df950fc44c6237ec09c557a1","9007199254740992":"03a3c0e9da7ece7d7b132c53662c0389bd87db801dff5ac9edd9f46699cb1dc065","18014398509481984":"03b6c4c874e2392072e17fbfd181afbd40d6766a8ca4cf932264ba98d98de1328c","36028797018963968":"0370dca4416ec6e30ff02f8e9db7804348b42e3f5c22099dfc896fa1b2ccbe7a69","72057594037927936":"0226250140aedb79de91cb4cc7350884bde229063f34ee0849081bb391a37c273e","144115188075855872":"02baef3a94d241aee9d6057c7a7ee7424f8a0bcb910daf6c49ddcabf70ffbc77d8","288230376151711744":"030f95a12369f1867ce0dbf2a6322c27d70c61b743064d76cfc81dd43f1a052ae6","576460752303423488":"021bc89118ab6eb1fbebe0fa6cc76da8236a7991163475a73a22d8efd016a45800","1152921504606846976":"03b0c1e658d7ca12830a0b590ea5a4d6db51084ae80b6d8abf27ad2d762209acd1","2305843009213693952":"0266926ce658a0bdae934071f22e09dbb6ecaff2a4dc4b1f8e23626570d993b48e","4611686018427387904":"03ac17f10f9bb745ebd8ee9cdca1b6981f5a356147d431196c21c6d4869402bde0","9223372036854775808":"037ab5b88c8ce34c4a3970be5c6f75b8a7a5493a12ef56a1c9ba9ff5f90de46fcc"}},{"id":"000f01df73ea149a","unit":"sat","keys":{"1":"03ba786a2c0745f8c30e490288acd7a72dd53d65afd292ddefa326a4a3fa14c566","2":"03361cd8bd1329fea797a6add1cf1990ffcf2270ceb9fc81eeee0e8e9c1bd0cdf5","4":"036e378bcf78738ddf68859293c69778035740e41138ab183c94f8fee7572214c7","8":"03909d73beaf28edfb283dbeb8da321afd40651e8902fcf5454ecc7d69788626c0","16":"028a36f0e6638ea7466665fe174d958212723019ec08f9ce6898d897f88e68aa5d","32":"03a97a40e146adee2687ac60c2ba2586a90f970de92a9d0e6cae5a4b9965f54612","64":"03ce86f0c197aab181ddba0cfc5c5576e11dfd5164d9f3d4a3fc3ffbbf2e069664","128":"0284f2c06d938a6f78794814c687560a0aabab19fe5e6f30ede38e113b132a3cb9","256":"03b99f475b68e5b4c0ba809cdecaae64eade2d9787aa123206f91cd61f76c01459","512":"03d4db82ea19a44d35274de51f78af0a710925fe7d9e03620b84e3e9976e3ac2eb","1024":"031fbd4ba801870871d46cf62228a1b748905ebc07d3b210daf48de229e683f2dc","2048":"0276cedb9a3b160db6a158ad4e468d2437f021293204b3cd4bf6247970d8aff54b","4096":"02fc6b89b403ee9eb8a7ed457cd3973638080d6e04ca8af7307c965c166b555ea2","8192":"0320265583e916d3a305f0d2687fcf2cd4e3cd03a16ea8261fda309c3ec5721e21","16384":"036e41de58fdff3cb1d8d713f48c63bc61fa3b3e1631495a444d178363c0d2ed50","32768":"0365438f613f19696264300b069d1dad93f0c60a37536b72a8ab7c7366a5ee6c04","65536":"02408426cfb6fc86341bac79624ba8708a4376b2d92debdf4134813f866eb57a8d","131072":"031063e9f11c94dc778c473e968966eac0e70b7145213fbaff5f7a007e71c65f41","262144":"02f2a3e808f9cd168ec71b7f328258d0c1dda250659c1aced14c7f5cf05aab4328","524288":"038ac10de9f1ff9395903bb73077e94dbf91e9ef98fd77d9a2debc5f74c575bc86","1048576":"0203eaee4db749b0fc7c49870d082024b2c31d889f9bc3b32473d4f1dfa3625788","2097152":"033cdb9d36e1e82ae652b7b6a08e0204569ec7ff9ebf85d80a02786dc7fe00b04c","4194304":"02c8b73f4e3a470ae05e5f2fe39984d41e9f6ae7be9f3b09c9ac31292e403ac512","8388608":"025bbe0cfce8a1f4fbd7f3a0d4a09cb6badd73ef61829dc827aa8a98c270bc25b0","16777216":"037eec3d1651a30a90182d9287a5c51386fe35d4a96839cf7969c6e2a03db1fc21","33554432":"03280576b81a04e6abd7197f305506476f5751356b7643988495ca5c3e14e5c262","67108864":"03268bfb05be1dbb33ab6e7e00e438373ca2c9b9abc018fdb452d0e1a0935e10d3","134217728":"02573b68784ceba9617bbcc7c9487836d296aa7c628c3199173a841e7a19798020","268435456":"0234076b6e70f7fbf755d2227ecc8d8169d662518ee3a1401f729e2a12ccb2b276","536870912":"03015bd88961e2a466a2163bd4248d1d2b42c7c58a157e594785e7eb34d880efc9","1073741824":"02c9b076d08f9020ebee49ac8ba2610b404d4e553a4f800150ceb539e9421aaeee","2147483648":"034d592f4c366afddc919a509600af81b489a03caf4f7517c2b3f4f2b558f9a41a","4294967296":"037c09ecb66da082981e4cbdb1ac65c0eb631fc75d85bed13efb2c6364148879b5","8589934592":"02b4ebb0dda3b9ad83b39e2e31024b777cc0ac205a96b9a6cfab3edea2912ed1b3","17179869184":"026cc4dacdced45e63f6e4f62edbc5779ccd802e7fabb82d5123db879b636176e9","34359738368":"02b2cee01b7d8e90180254459b8f09bbea9aad34c3a2fd98c85517ecfc9805af75","68719476736":"037a0c0d564540fc574b8bfa0253cca987b75466e44b295ed59f6f8bd41aace754","137438953472":"021df6585cae9b9ca431318a713fd73dbb76b3ef5667957e8633bca8aaa7214fb6","274877906944":"02b8f53dde126f8c85fa5bb6061c0be5aca90984ce9b902966941caf963648d53a","549755813888":"029cc8af2840d59f1d8761779b2496623c82c64be8e15f9ab577c657c6dd453785","1099511627776":"03e446fdb84fad492ff3a25fc1046fb9a93a5b262ebcd0151caa442ea28959a38a","2199023255552":"02d6b25bd4ab599dd0818c55f75702fde603c93f259222001246569018842d3258","4398046511104":"03397b522bb4e156ec3952d3f048e5a986c20a00718e5e52cd5718466bf494156a","8796093022208":"02d1fb9e78262b5d7d74028073075b80bb5ab281edcfc3191061962c1346340f1e","17592186044416":"030d3f2ad7a4ca115712ff7f140434f802b19a4c9b2dd1c76f3e8e80c05c6a9310","35184372088832":"03e325b691f292e1dfb151c3fb7cad440b225795583c32e24e10635a80e4221c06","70368744177664":"03bee8f64d88de3dee21d61f89efa32933da51152ddbd67466bef815e9f93f8fd1","140737488355328":"0327244c9019a4892e1f04ba3bf95fe43b327479e2d57c25979446cc508cd379ed","281474976710656":"02fb58522cd662f2f8b042f8161caae6e45de98283f74d4e99f19b0ea85e08a56d","562949953421312":"02adde4b466a9d7e59386b6a701a39717c53f30c4810613c1b55e6b6da43b7bc9a","1125899906842624":"038eeda11f78ce05c774f30e393cda075192b890d68590813ff46362548528dca9","2251799813685248":"02ec13e0058b196db80f7079d329333b330dc30c000dbdd7397cbbc5a37a664c4f","4503599627370496":"02d2d162db63675bd04f7d56df04508840f41e2ad87312a3c93041b494efe80a73","9007199254740992":"0356969d6aef2bb40121dbd07c68b6102339f4ea8e674a9008bb69506795998f49","18014398509481984":"02f4e667567ebb9f4e6e180a4113bb071c48855f657766bb5e9c776a880335d1d6","36028797018963968":"0385b4fe35e41703d7a657d957c67bb536629de57b7e6ee6fe2130728ef0fc90b0","72057594037927936":"02b2bc1968a6fddbcc78fb9903940524824b5f5bed329c6ad48a19b56068c144fd","144115188075855872":"02e0dbb24f1d288a693e8a49bc14264d1276be16972131520cf9e055ae92fba19a","288230376151711744":"03efe75c106f931a525dc2d653ebedddc413a2c7d8cb9da410893ae7d2fa7d19cc","576460752303423488":"02c7ec2bd9508a7fc03f73c7565dc600b30fd86f3d305f8f139c45c404a52d958a","1152921504606846976":"035a6679c6b25e68ff4e29d1c7ef87f21e0a8fc574f6a08c1aa45ff352c1d59f06","2305843009213693952":"033cdc225962c052d485f7cfbf55a5b2367d200fe1fe4373a347deb4cc99e9a099","4611686018427387904":"024a4b806cf413d14b294719090a9da36ba75209c7657135ad09bc65328fba9e6f","9223372036854775808":"0377a6fe114e291a8d8e991627c38001c8305b23b9e98b1c7b1893f5cd0dda6cad"}},{"id":"yjzQhxghPdrr","unit":"sat","keys":{"1":"038a935c51c76c780ff9731cfbe9ab477f38346775809fa4c514340feabbec4b3a","2":"038288b12ebf2db3645e5d58835bd100398b6b19dfef338c698b55c05d0d41fb0a","4":"02fc8201cf4ea29abac0495d1304064f0e698762b8c0db145c1737b38a9d61c7e2","8":"02274243e03ca19f969acc7072812405b38adc672d1d753e65c63746b3f31cc6eb","16":"025f07cb2493351e7d5202f05eaf3934d5c9d17e73385e9de5bfab802f7d8caf92","32":"03afce0a897c858d7c88c1454d492eac43011e3396dda5b778ba1fcab381c748b1","64":"037b2178f42507f0c95e09d9b435a127df4b3e23ccd20af8075817d3abe90947ad","128":"02ebce8457b48407d4d248dba5a31b3eabf08a6285d09d08e40681c4adaf77bd40","256":"03c89713d27d6f8e328597b43dd87623efdcb251a484932f9e095ebfb6dbf4bdf2","512":"02df10f3ebba69916d03ab1754488770498f2e5466224d6df6d12811a13e46776c","1024":"02f5d9cba0502c21c6b39938a09dcb0390f124a2fd65e45dfeccd153cc1864273d","2048":"039de1dad91761b194e7674fb6ba212241aaf7f49dcb578a8fe093196ad1b20d1c","4096":"03cc694ba22e455f1c22b2cee4a40ecdd4f3bb4da0745411adb456158372d3efbb","8192":"029d66c24450fc315e046010df6870d61daa90c5c486c5ec4d7d3b99c5c2bce923","16384":"0387d063821010c7bd5cf79441870182f70cd432d13d3fc255e7b6ffd82c9d3c5a","32768":"021a94c6c03f7de8feb25b8a8b8d1f1c6f56af4bc533eb97c9e8b89c76b616ff11","65536":"038989c6ed91a7c577953115b465ee400a270a64e95eda8f7ee9d6bf30b8fe4908","131072":"03c3d3cd2523f004ee479a170b0ec5c74c060edb8356fc1b0a9ed8087cf6345172","262144":"02e54a7546f1a9194f30baa593a13d4e2949eb866593445d89675d7d394ef6320b","524288":"034e91037b3f1d3258d1e871dede80e98ef83e307c2e5ff589f38bd046f97546f8","1048576":"03306d42752a1adcfa394af2a690961ac9b80b1ac0f5fdc0890f66f8dc7d25ac6e","2097152":"03ec114332fe798c3e36675566c4748fda7d881000a01864ec48486512d7901e76","4194304":"02095e3e443d98ca3dfabcebc2f9154f3656b889783f7edb8290cfb01f497e63cf","8388608":"03c90f31525a4f9ab6562ec3edbf2bafc6662256ea6ce82ab19a45d2aee80b2f15","16777216":"03c0ae897a45724465c713c1379671ac5ff0a81c32e5f2dd27ea7e5530c7af484c","33554432":"034bcf793b70ba511e9c84cd07fc0c73c061e912bc02df4cac7871d048bad653b6","67108864":"021c6826c23a181d14962f43121943569a54f9d5af556eb839aee42d3f62debee6","134217728":"030e1bc651b6496922978d6cd3ed923cbf12b4332c496f841f506f5abf9d186d35","268435456":"03e3219e50cf389a75794f82ab4f880f5ffe9ca227b992c3e93cb4bb659d8e3353","536870912":"03879ad42536c410511ac6956b9da2d0da59ce7fbb6068bd9b25dd7cccddcc8096","1073741824":"03c4d3755a17904c0cfa7d7a21cc5b4e85fca8ac85369fcb12a6e2177525117dee","2147483648":"02e7a5d5cd3ea24f05f741dddad3dc8c5e24db60eb9bf9ad888b1c5dfbd792665e","4294967296":"03c783d24d8c9e51207eb3d6199bf48d6eb81a4b34103b422724be15501ff921bd","8589934592":"03200234495725455f4c4e6b6cb7b7936eb7cd1d1c9bb73d2ce032bae7d728b3ca","17179869184":"02eafa50ac67de2c206d1a67245b72ec20fac081c2a550294cc0a711246ed65a41","34359738368":"024c153c2a56de05860006aff9dc35ec9cafd7ac68708442a3a326c858b0c1a146","68719476736":"035a890c2d5c8bf259b98ac67d0d813b87778bcb0c0ea1ee9717ac804b0be3f563","137438953472":"025184ca832f08b105fdb471e2caf14025a1daa6f44ce90b4c7703878ccb6b26e8","274877906944":"039d19a41abdd49949c60672430018c63f27c5a28991f9fbb760499daccc63146c","549755813888":"03a138ac626dd3e6753459903aa128a13c052ed0058f2ead707c203bd4a7565237","1099511627776":"0298c8ef2eab728613103481167102efaf2d4b7a303cb94b9393da37a034a95c53","2199023255552":"02d88f8fc93cd2edf303fdebfecb70e59b5373cb8f746a1d075a9c86bc9382ac07","4398046511104":"02afd89ee23eee7d5fe6687fee898f64e9b01913ec71b5c596762b215e040c701f","8796093022208":"02196b461f3c804259e597c50e514920427aab4beaef0c666185fb2ff4399813db","17592186044416":"037b33746a6fd7a71d4cf17c85d13a64b98620614c0028d4995163f1b8484ee337","35184372088832":"036cce0a1878bbc63b3108c379ef4e6529fbf20ed675d80d91ca3ccc55fde4bdbd","70368744177664":"039c81dccb319ba70597cdf9db33b459164a1515c27366c8f667b01d988874e554","140737488355328":"036b2dd85a3c44c4458f0b246ce19a1524a191f1716834cfb452c6e1f946172c19","281474976710656":"022c84722c31a2b3d8cfd9b6a9e6199515fd97d6a9c390fc3d82f123bfc501ad04","562949953421312":"0355e2be85ee599b8fa7e6e68a9954573d032e89aa9e65c2e1231991664c200bf3","1125899906842624":"024b10818cd27f3eec6c9daf82b9dfa53928ab0711b711070bd39892ac10dee765","2251799813685248":"02a6d726432bb18c3145eba4fc0b587bf64f3be8617c0070dda33944474b3f8740","4503599627370496":"0248304be3cbaf31ec320bc636bb936c5984caf773df950fc44c6237ec09c557a1","9007199254740992":"03a3c0e9da7ece7d7b132c53662c0389bd87db801dff5ac9edd9f46699cb1dc065","18014398509481984":"03b6c4c874e2392072e17fbfd181afbd40d6766a8ca4cf932264ba98d98de1328c","36028797018963968":"0370dca4416ec6e30ff02f8e9db7804348b42e3f5c22099dfc896fa1b2ccbe7a69","72057594037927936":"0226250140aedb79de91cb4cc7350884bde229063f34ee0849081bb391a37c273e","144115188075855872":"02baef3a94d241aee9d6057c7a7ee7424f8a0bcb910daf6c49ddcabf70ffbc77d8","288230376151711744":"030f95a12369f1867ce0dbf2a6322c27d70c61b743064d76cfc81dd43f1a052ae6","576460752303423488":"021bc89118ab6eb1fbebe0fa6cc76da8236a7991163475a73a22d8efd016a45800","1152921504606846976":"03b0c1e658d7ca12830a0b590ea5a4d6db51084ae80b6d8abf27ad2d762209acd1","2305843009213693952":"0266926ce658a0bdae934071f22e09dbb6ecaff2a4dc4b1f8e23626570d993b48e","4611686018427387904":"03ac17f10f9bb745ebd8ee9cdca1b6981f5a356147d431196c21c6d4869402bde0","9223372036854775808":"037ab5b88c8ce34c4a3970be5c6f75b8a7a5493a12ef56a1c9ba9ff5f90de46fcc"}}]} + ; + + const resp = try std.json.parseFromSlice(nut01.KeysResponse, std.testing.allocator, json, .{}); + defer resp.deinit(); + + try std.testing.expectEqual(2, resp.value.keysets.len); +} diff --git a/src/core/nuts/nut03/nut03.zig b/src/core/nuts/nut03/nut03.zig new file mode 100644 index 0000000..5bd7e7b --- /dev/null +++ b/src/core/nuts/nut03/nut03.zig @@ -0,0 +1,59 @@ +//! NUT-03: Swap +//! +//! + +const BlindSignature = @import("../nut00/lib.zig").BlindSignature; +const BlindedMessage = @import("../nut00/lib.zig").BlindedMessage; +// const PreMintSecrets = @import("../nut00/lib.zig").PreMintSecrets; +const Proof = @import("../nut00/lib.zig").Proof; + +// /// Preswap information +// pub const PreSwap = struct { +// /// Preswap mint secrets +// pre_mint_secrets: PreMintSecrets, +// /// Swap request +// swap_request: SwapRequest, +// /// Amount to increment keyset counter by +// derived_secret_count: u32, +// /// Fee amount +// fee: u64, +// }; + +/// Split Request [NUT-06] +pub const SwapRequest = struct { + /// Proofs that are to be spent in `Split` + inputs: []const Proof, + /// Blinded Messages for Mint to sign + outputs: []const BlindedMessage, + + /// Total value of proofs in [`SwapRequest`] + pub fn inputAmount(self: SwapRequest) u64 { + var sum: u64 = 0; + for (self.inputs) |proof| sum += proof.amount; + + return sum; + } + + /// Total value of outputs in [`SwapRequest`] + pub fn outputAmount(self: SwapRequest) u64 { + var sum: u64 = 0; + for (self.outputs) |proof| sum += proof.amount; + + return sum; + } +}; + +/// Split Response [NUT-06] +pub const SwapResponse = struct { + /// Promises + signatures: []const BlindSignature, + + /// Total [`Amount`] of promises + pub fn promisesAmount(self: SwapResponse) u64 { + var sum: u64 = 0; + + for (self.signatures) |bs| sum += bs.amount; + + return sum; + } +}; diff --git a/src/core/nuts/nut04/nut04.zig b/src/core/nuts/nut04/nut04.zig new file mode 100644 index 0000000..a061dd0 --- /dev/null +++ b/src/core/nuts/nut04/nut04.zig @@ -0,0 +1,38 @@ +//! NUT-04: Mint Tokens via Bolt11 +//! +//! +const std = @import("std"); +const CurrencyUnit = @import("../nut00/lib.zig").CurrencyUnit; +const Proof = @import("../nut00/lib.zig").Proof; +const PaymentMethod = @import("../nut00/lib.zig").PaymentMethod; + +pub const QuoteState = enum { + /// Quote has not been paid + unpaid, + /// Quote has been paid and wallet can mint + paid, + /// Minting is in progress + /// **Note:** This state is to be used internally but is not part of the nut. + pending, + /// ecash issued for quote + issued, +}; + +pub const MintMethodSettings = struct { + /// Payment Method e.g. bolt11 + method: PaymentMethod, + /// Currency Unit e.g. sat + unit: CurrencyUnit = .sat, + /// Min Amount + min_amount: ?u64 = null, + /// Max Amount + max_amount: ?u64 = null, +}; + +/// Mint Settings +pub const Settings = struct { + /// Methods to mint + methods: []const MintMethodSettings = &.{}, + /// Minting disabled + disabled: bool = false, +}; diff --git a/src/core/nuts/nut05/nut05.zig b/src/core/nuts/nut05/nut05.zig new file mode 100644 index 0000000..d702ed1 --- /dev/null +++ b/src/core/nuts/nut05/nut05.zig @@ -0,0 +1,147 @@ +//! NUT-05: Melting Tokens +//! +//! +const BlindSignature = @import("../nut00/lib.zig").BlindSignature; +const BlindedMessage = @import("../nut00/lib.zig").BlindedMessage; +const CurrencyUnit = @import("../nut00/lib.zig").CurrencyUnit; +const Proof = @import("../nut00/lib.zig").Proof; +const PaymentMethod = @import("../nut00/lib.zig").PaymentMethod; +const Mpp = @import("../nut15/nut15.zig").Mpp; +const Bolt11Invoice = @import("../../../mint/lightning/invoices/lib.zig").Bolt11Invoice; +const std = @import("std"); + +/// Melt quote request [NUT-05] +pub const MeltQuoteBolt11Request = struct { + /// Bolt11 invoice to be paid + request: Bolt11Invoice, + /// Unit wallet would like to pay with + unit: CurrencyUnit, + /// Payment Options + options: ?Mpp = null, +}; + +pub const QuoteState = enum { + /// Quote has not been paid + unpaid, + /// Quote has been paid + paid, + /// Paying quote is in progress + pending, + + pub fn toString(self: QuoteState) []const u8 { + return switch (self) { + .unpaid => "UNPAID", + .paid => "PAID", + .pending => "PENDING", + }; + } + + pub fn fromString(s: []const u8) !QuoteState { + const kv = std.StaticStringMap(QuoteState).initComptime( + &.{ + .{ "UNPAID", QuoteState.unpaid }, + .{ "PAID", QuoteState.paid }, + .{ "PENDING", QuoteState.pending }, + }, + ); + + return kv.get(s) orelse return error.UnknownState; + } +}; + +/// Melt quote response [NUT-05] +pub const MeltQuoteBolt11Response = struct { + /// Quote Id + quote: []const u8, + /// The amount that needs to be provided + amount: u64, + /// The fee reserve that is required + fee_reserve: u64, + /// Whether the the request haas be paid + // TODO: To be deprecated + /// Deprecated + paid: ?bool, + /// Quote State + state: QuoteState, + /// Unix timestamp until the quote is valid + expiry: u64, + /// Payment preimage + payment_preimage: ?[]const u8 = null, + /// Change + change: ?[]const BlindSignature = null, +}; + +/// Melt Bolt11 Request [NUT-05] +pub const MeltBolt11Request = struct { + /// Quote ID + quote: []const u8, + /// Proofs + inputs: []const Proof, + /// Blinded Message that can be used to return change [NUT-08] + /// Amount field of BlindedMessages `SHOULD` be set to zero + outputs: ?[]const BlindedMessage = null, + + /// Total [`Amount`] of [`Proofs`] + pub fn proofsAmount(self: MeltBolt11Request) u64 { + var sum: u64 = 0; + for (self.inputs) |proof| { + sum += proof.amount; + } + + return sum; + } +}; + +// TODO: to be deprecated +/// Melt Response [NUT-05] +pub const MeltBolt11Response = struct { + /// Indicate if payment was successful + paid: bool, + /// Bolt11 preimage + payment_preimage: ?[]const u8 = null, + /// Change + change: ?[]const BlindSignature, + // impl From for MeltBolt11Response { +}; + +/// Melt Method Settings +pub const MeltMethodSettings = struct { + /// Payment Method e.g. bolt11 + method: PaymentMethod, + /// Currency Unit e.g. sat + unit: CurrencyUnit = .sat, + /// Min Amount + min_amount: ?u64 = null, + /// Max Amount + max_amount: ?u64 = null, +}; + +/// Melt Settings +pub const Settings = struct { + /// Methods to melt + methods: []const MeltMethodSettings = &.{ + .{ + .method = .bolt11, + .unit = .sat, + .min_amount = 1, + .max_amount = 1000000, + }, + }, + /// Minting disabled + disabled: bool = false, + + // /// Get [`MeltMethodSettings`] for unit method pair + // pub fn get_settings( + // &self, + // unit: &CurrencyUnit, + // method: &PaymentMethod, + // ) -> Option { + // for method_settings in self.methods.iter() { + // if method_settings.method.eq(method) && method_settings.unit.eq(unit) { + // return Some(method_settings.clone()); + // } + // } + + // None + // } +}; diff --git a/src/core/nuts/nut06/nut06.zig b/src/core/nuts/nut06/nut06.zig new file mode 100644 index 0000000..48f680d --- /dev/null +++ b/src/core/nuts/nut06/nut06.zig @@ -0,0 +1,234 @@ +//! NUT-06: Mint Information +//! +//! +const std = @import("std"); +const PublicKey = @import("../../secp256k1.zig").PublicKey; +const MppMethodSettings = @import("../nut15/nut15.zig").MppMethodSettings; +const nut05 = @import("../nut05/nut05.zig"); +const nut15 = @import("../nut15/nut15.zig"); +const nut04 = @import("../nut04/nut04.zig"); +const helper = @import("../../../helper/helper.zig"); + +/// Mint Version +pub const MintVersion = struct { + /// Mint Software name + name: []const u8, + /// Mint Version + version: []const u8, + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + const value = try std.json.innerParse([]const u8, allocator, source, options); + + var parts = std.mem.splitScalar(u8, value, '/'); + + return .{ + .name = parts.next().?, + .version = parts.next().?, + }; + } + + pub fn jsonStringify(self: MintVersion, out: anytype) !void { + try out.print("\"{s}/{s}\"", .{ self.name, self.version }); + } +}; + +/// Supported nuts and settings +pub const Nuts = struct { + /// NUT04 Settings + nut04: nut04.Settings = .{}, + /// NUT05 Settings + nut05: nut05.Settings = .{}, + /// NUT07 Settings + nut07: SupportedSettings = .{}, + /// NUT08 Settings + nut08: SupportedSettings = .{}, + /// NUT09 Settings + nut09: SupportedSettings = .{}, + /// NUT10 Settings + nut10: SupportedSettings = .{}, + /// NUT11 Settings + nut11: SupportedSettings = .{}, + /// NUT12 Settings + nut12: SupportedSettings = .{}, + /// NUT14 Settings + nut14: SupportedSettings = .{}, + /// NUT15 Settings + nut15: nut15.Settings = .{}, + + pub usingnamespace helper.RenameJsonField( + @This(), + std.StaticStringMap([]const u8).initComptime( + &.{ + .{ + "nut04", "4", + }, + .{ + "nut05", "5", + }, + + .{ + "nut07", "7", + }, + .{ + "nut08", "8", + }, + .{ + "nut09", "9", + }, + .{ + "nut10", "10", + }, + .{ + "nut11", "11", + }, + .{ + "nut12", "12", + }, + .{ + "nut14", "14", + }, + .{ + "nut15", "15", + }, + }, + ), + ); +}; + +/// Mint Info [NIP-06] +pub const MintInfo = struct { + /// name of the mint and should be recognizable + name: ?[]const u8 = null, + /// hex pubkey of the mint + pubkey: ?PublicKey = null, + /// implementation name and the version running + version: ?MintVersion = null, + /// short description of the mint + description: ?[]const u8 = null, + /// long description + description_long: ?[]const u8 = null, + /// Contact info + contact: ?[]const ContactInfo = null, + /// shows which NUTs the mint supports + nuts: Nuts, + /// Mint's icon URL + mint_icon_url: ?[]const u8 = null, + /// message of the day that the wallet must display to the user + motd: ?[]const u8 = null, +}; + +/// Check state Settings +pub const SupportedSettings = struct { + supported: bool = false, +}; + +/// Contact Info +pub const ContactInfo = struct { + /// Contact Method i.e. nostr + method: []const u8, + /// Contact info i.e. npub... + info: []const u8, +}; + +test "test_des_mint_into" { + const mint_info_str = + \\ { + \\ "name": "Cashu mint", + \\ "pubkey": "0296d0aa13b6a31cf0cd974249f28c7b7176d7274712c95a41c7d8066d3f29d679", + \\ "version": "Nutshell/0.15.3", + \\ "contact": [ + \\ {"method": "", "info": ""}, + \\ {"method": "", "info": ""} + \\ ], + \\ "nuts": { + \\ "4": { + \\ "methods": [ + \\ {"method": "bolt11", "unit": "sat"}, + \\ {"method": "bolt11", "unit": "usd"} + \\ ], + \\ "disabled": false + \\ }, + \\ "5": { + \\ "methods": [ + \\ {"method": "bolt11", "unit": "sat"}, + \\ {"method": "bolt11", "unit": "usd"} + \\ ], + \\ "disabled": false + \\ }, + \\ "7": {"supported": true}, + \\ "8": {"supported": true}, + \\ "9": {"supported": true}, + \\ "10": {"supported": true}, + \\ "11": {"supported": true} + \\ } + \\} + ; + const mint_info = try std.json.parseFromSlice(MintInfo, std.testing.allocator, mint_info_str, .{ + .ignore_unknown_fields = true, + }); + defer mint_info.deinit(); +} +test "test_ser_mint_into" { + const mint_info_str = + \\{ + \\"name": "Bob's Cashu mint", + \\"pubkey": "0283bf290884eed3a7ca2663fc0260de2e2064d6b355ea13f98dec004b7a7ead99", + \\"version": "Nutshell/0.15.0", + \\"description": "The short mint description", + \\"description_long": "A description that can be a long piece of text.", + \\"contact": [ + \\ { + \\ "method": "nostr", + \\ "info": "xxxxx" + \\ }, + \\ { + \\ "method": "email", + \\ "info": "contact@me.com" + \\ } + \\] , + \\"motd": "Message to display to users.", + \\"mint_icon_url": "https://this-is-a-mint-icon-url.com/icon.png", + \\"nuts": { + \\ "4": { + \\ "methods": [ + \\ { + \\ "method": "bolt11", + \\ "unit": "sat", + \\ "min_amount": 0, + \\ "max_amount": 10000 + \\ } + \\ ], + \\ "disabled": false + \\ }, + \\ "5": { + \\ "methods": [ + \\ { + \\ "method": "bolt11", + \\ "unit": "sat", + \\ "min_amount": 0, + \\ "max_amount": 10000 + \\ } + \\ ], + \\ "disabled": false + \\ }, + \\ "7": {"supported": true}, + \\ "8": {"supported": true}, + \\ "9": {"supported": true}, + \\ "10": {"supported": true}, + \\ "12": {"supported": true} + \\} + \\} + ; + const mint_info = try std.json.parseFromSlice(MintInfo, std.testing.allocator, mint_info_str, .{}); + defer mint_info.deinit(); + + var result_json = std.ArrayList(u8).init(std.testing.allocator); + defer result_json.deinit(); + + try std.json.stringify(&mint_info.value, .{}, result_json.writer()); + + var mint_info_2 = try std.json.parseFromSlice(MintInfo, std.testing.allocator, result_json.items, .{}); + defer mint_info_2.deinit(); + + try std.testing.expectEqualDeep(mint_info.value, mint_info_2.value); +} diff --git a/src/core/nuts/nut07/nut07.zig b/src/core/nuts/nut07/nut07.zig new file mode 100644 index 0000000..2c80350 --- /dev/null +++ b/src/core/nuts/nut07/nut07.zig @@ -0,0 +1,88 @@ +//! NUT-07: Spendable Check +//! +//! +const std = @import("std"); +const helper = @import("../../../helper/helper.zig"); +const secp256k1 = @import("../../secp256k1.zig"); + +/// State of Proof +pub const State = enum { + /// Spent + spent, + /// Unspent + unspent, + /// Pending + /// + /// Currently being used in a transaction i.e. melt in progress + pending, + /// Proof is reserved + /// + /// i.e. used to create a token + reserved, + + pub fn toString(self: State) []const u8 { + return switch (self) { + .spent => "SPENT", + .unspent => "UNSPENT", + .pending => "PENDING", + .reserved => "RESERVED", + }; + } + + pub fn fromString(s: []const u8) !State { + const kv = std.StaticStringMap(State).initComptime( + &.{ + .{ "SPENT", State.spent }, + .{ "UNSPENT", State.unspent }, + .{ "RESERVED", State.reserved }, + .{ "PENDING", State.pending }, + }, + ); + + return kv.get(s) orelse return error.UnknownState; + } +}; + +/// Check spendabale request [NUT-07] +pub const CheckStateRequest = struct { + /// Y's of the proofs to check + ys: []const secp256k1.PublicKey, + + pub usingnamespace helper.RenameJsonField( + @This(), + std.StaticStringMap([]const u8).initComptime( + &.{ + .{ + "ys", "Ys", + }, + }, + ), + ); +}; + +/// Proof state [NUT-07] +pub const ProofState = struct { + /// Y of proof + y: secp256k1.PublicKey, + /// State of proof + state: State, + /// Witness data if it is supplied + witness: ?[]const u8, + + pub usingnamespace helper.RenameJsonField( + @This(), + std.StaticStringMap([]const u8).initComptime( + &.{ + .{ + "y", "Y", + }, + }, + ), + ); +}; + +/// Check Spendable Response [NUT-07] +pub const CheckStateResponse = struct { + /// Proof states + states: []const ProofState, +}; diff --git a/src/core/nuts/nut08/nut08.zig b/src/core/nuts/nut08/nut08.zig new file mode 100644 index 0000000..bff62d3 --- /dev/null +++ b/src/core/nuts/nut08/nut08.zig @@ -0,0 +1,24 @@ +//! NUT-08: Lightning fee return +//! +//! +const MeltBolt11Request = @import("../nut05/nut05.zig").MeltBolt11Request; +const MeltQuoteBolt11Response = @import("../nut05/nut05.zig").MeltQuoteBolt11Response; + +/// Total output [`Amount`] for [`MeltBolt11Request`] +pub fn outputAmount(self: MeltBolt11Request) ?u64 { + var sum: u64 = 0; + for (self.outputs orelse return null) |proof| { + sum += proof.amount; + } + return sum; +} + +/// Total change [`Amount`] for [`MeltQuoteBolt11Response`] +pub fn changeAmount(self: MeltQuoteBolt11Response) ?u64 { + var sum: u64 = 0; + for (self.change orelse return null) |b| { + sum += b.amount; + } + + return sum; +} diff --git a/src/core/nuts/nut09/nut09.zig b/src/core/nuts/nut09/nut09.zig new file mode 100644 index 0000000..9ed9699 --- /dev/null +++ b/src/core/nuts/nut09/nut09.zig @@ -0,0 +1,46 @@ +//! NUT-09: Restore signatures +//! +//! +const std = @import("std"); +const BlindedMessage = @import("../nut00/lib.zig").BlindedMessage; +const BlindSignature = @import("../nut00/lib.zig").BlindSignature; +const helper = @import("../../../helper/helper.zig"); + +/// Restore Request [NUT-09] +pub const RestoreRequest = struct { + /// Outputs + outputs: []const BlindedMessage, +}; + +/// Restore Response [NUT-09] +pub const RestoreResponse = struct { + /// Outputs + outputs: []const BlindedMessage, + /// Signatures + signatures: []const BlindSignature, + + pub usingnamespace helper.RenameJsonField( + @This(), + std.StaticStringMap([]const u8).initComptime( + &.{ + .{ + "signatures", "promises", + }, + }, + ), + ); +}; + +test "restore_response" { + const rs = + \\{"outputs":[{"B_":"0204bbffa045f28ec836117a29ea0a00d77f1d692e38cf94f72a5145bfda6d8f41","amount":0,"id":"00ffd48b8f5ecf80", "witness":null},{"B_":"025f0615ccba96f810582a6885ffdb04bd57c96dbc590f5aa560447b31258988d7","amount":0,"id":"00ffd48b8f5ecf80"}],"promises":[{"C_":"02e9701b804dc05a5294b5a580b428237a27c7ee1690a0177868016799b1761c81","amount":8,"dleq":null,"id":"00ffd48b8f5ecf80"},{"C_":"031246ee046519b15648f1b8d8ffcb8e537409c84724e148c8d6800b2e62deb795","amount":2,"dleq":null,"id":"00ffd48b8f5ecf80"}]} + ; + + const res = try std.json.parseFromSlice(RestoreResponse, std.testing.allocator, rs, .{}); + defer res.deinit(); + + try std.testing.expectEqual(res.value.signatures.len, 2); + try std.testing.expectEqual(res.value.outputs.len, 2); + + // std.log.warn("res: {any}", .{res.value}); +} diff --git a/src/core/nuts/nut10/nut10.zig b/src/core/nuts/nut10/nut10.zig new file mode 100644 index 0000000..e67540c --- /dev/null +++ b/src/core/nuts/nut10/nut10.zig @@ -0,0 +1,171 @@ +//! NUT-10: Spending conditions +//! +//! + +const std = @import("std"); +const secret_lib = @import("../../secret.zig"); +const helper = @import("../../../helper/helper.zig"); + +/// NUT10 Secret Kind +pub const Kind = enum { + /// NUT-11 P2PK + p2pk, + /// NUT-14 HTLC + htlc, + + pub fn jsonStringify(self: Kind, out: anytype) !void { + try out.write(switch (self) { + .p2pk => "P2PK", + .htlc => "HTLC", + }); + } + + pub fn jsonParse(_: std.mem.Allocator, source: anytype, _: std.json.ParseOptions) !Kind { + const name = switch (try source.next()) { + inline .string, .allocated_string => |slice| slice, + else => return error.UnexpectedToken, + }; + + if (std.mem.eql(u8, name, "P2PK")) return .p2pk; + if (std.mem.eql(u8, name, "HTLC")) return .htlc; + + return error.UnexpectedToken; + } +}; + +/// Secert Date +pub const SecretData = struct { + /// Unique random string + nonce: []const u8, + /// Expresses the spending condition specific to each kind + data: []const u8, + /// Additional data committed to and can be used for feature extensions + tags: ?[]const []const []const u8, + + pub fn eql(self: SecretData, other: SecretData) bool { + if (!std.mem.eql(u8, self.nonce, other.nonce)) return false; + if (!std.mem.eql(u8, self.data, other.data)) return false; + + if ((self.tags != null and other.tags == null) or (self.tags == null and other.tags != null)) return false; + + if (self.tags) |stags| { + if (other.tags) |tags| { + if (stags.len != tags.len) return false; + + for (0..stags.len) |x| { + if (tags[x].len != stags[x].len) return false; + + for (0..tags[x].len) |i| { + if (!std.mem.eql(u8, tags[x][i], stags[x][i])) return false; + } + } + } + } + + return true; + } +}; + +/// NUT10 Secret +pub const Secret = struct { + /// Kind of the spending condition + kind: Kind, + /// Secret Data + secret_data: SecretData, + + arena: *std.heap.ArenaAllocator, + + pub fn deinit(self: *Secret) void { + const allocator = self.arena.child_allocator; + self.arena.deinit(); + allocator.destroy(self.arena); + } + + /// Create new [`Secret`] using allocator arena + pub fn init(alloc: std.mem.Allocator, kind: Kind, _data: []const u8, tags: ?std.ArrayList(std.ArrayList(std.ArrayList(u8)))) !Secret { + var arenaAllocator = try alloc.create(std.heap.ArenaAllocator); + errdefer alloc.destroy(arenaAllocator); + + arenaAllocator.* = std.heap.ArenaAllocator.init(alloc); + + errdefer arenaAllocator.deinit(); + const allocator = arenaAllocator.allocator(); + + const sec = try secret_lib.Secret.generate(allocator); + + const data = try allocator.alloc(u8, _data.len); + @memcpy(data, _data); + + const converted_tags = if (tags) |t| try helper.clone3dArrayToSlice(u8, allocator, t) else null; + + const secret_data = SecretData{ + .nonce = sec.toBytes(), + .data = data, + .tags = converted_tags, + }; + + return .{ + .kind = kind, + .arena = arenaAllocator, + .secret_data = secret_data, + }; + } + + pub fn jsonStringify(self: @This(), out: anytype) !void { + try out.beginArray(); + try out.write(self.kind); + try out.write(self.secret_data); + try out.endArray(); + } + + pub fn fromSecret(sec: secret_lib.Secret, allocator: std.mem.Allocator) !std.json.Parsed(Secret) { + return try std.json.parseFromSlice(Secret, allocator, sec.inner, .{}); + } + + pub fn toSecret(self: Secret, allocator: std.mem.Allocator) !secret_lib.Secret { + var output = std.ArrayList(u8).init(allocator); + errdefer output.deinit(); + + try std.json.stringify(&self, .{}, output.writer()); + + return secret_lib.Secret{ + .inner = try output.toOwnedSlice(), + }; + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !Secret { + if (try source.next() != .array_begin) return error.UnexpectedToken; + + var res: Secret = undefined; // no defaults + + res.kind = try std.json.innerParse(Kind, allocator, source, options); + res.secret_data = try std.json.innerParse(SecretData, allocator, source, options); + + // array_end + if (try source.next() != .array_end) return error.UnexpectedToken; + + return res; + } +}; + +test "test_secret_serialize" { + const secret = Secret{ + .kind = .p2pk, + .secret_data = .{ + .nonce = "5d11913ee0f92fefdc82a6764fd2457a", + .data = "026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198", + .tags = &.{&.{ "key", "value1", "value2" }}, + }, + .arena = undefined, + }; + + const secret_str = + \\["P2PK",{"nonce":"5d11913ee0f92fefdc82a6764fd2457a","data":"026562efcfadc8e86d44da6a8adf80633d974302e62c850774db1fb36ff4cc7198","tags":[["key","value1","value2"]]}] + ; + + var output = std.ArrayList(u8).init(std.testing.allocator); + defer output.deinit(); + try std.json.stringify(secret, .{}, output.writer()); + + try std.testing.expectEqualSlices(u8, secret_str, output.items); +} diff --git a/src/core/nuts/nut11/nut11.zig b/src/core/nuts/nut11/nut11.zig new file mode 100644 index 0000000..376d497 --- /dev/null +++ b/src/core/nuts/nut11/nut11.zig @@ -0,0 +1,734 @@ +//! NUT-11: Pay to Public Key (P2PK) +//! +//! +const std = @import("std"); + +const BlindedMessage = @import("../nut00/lib.zig").BlindedMessage; +const Proof = @import("../nut00/lib.zig").Proof; +const secp256k1 = @import("../../secp256k1.zig"); +const Witness = @import("../nut00/lib.zig").Witness; +const Nut10Secret = @import("../nut10/nut10.zig").Secret; +const Id = @import("../nut02/nut02.zig").Id; +const helper = @import("../../../helper/helper.zig"); +const zul = @import("zul"); + +/// P2Pk Witness +pub const P2PKWitness = struct { + /// Signatures + signatures: std.ArrayList(std.ArrayList(u8)), + + pub fn deinit(self: P2PKWitness) void { + for (self.signatures.items) |s| s.deinit(); + self.signatures.deinit(); + } +}; + +/// Spending Conditions +/// +/// Defined in [NUT10](https://github.com/cashubtc/nuts/blob/main/10.md) +pub const SpendingConditions = union(enum) { + /// NUT11 Spending conditions + /// + /// Defined in [NUT11](https://github.com/cashubtc/nuts/blob/main/11.md) + p2pk: struct { + /// The public key of the recipient of the locked ecash + data: secp256k1.PublicKey, + /// Additional Optional Spending [`Conditions`] + conditions: ?Conditions, + }, + /// NUT14 Spending conditions + /// + /// Dedined in [NUT14](https://github.com/cashubtc/nuts/blob/main/14.md) + htlc: struct { + /// Hash Lock of ecash + data: [32]u8, + /// Additional Optional Spending [`Conditions`] + conditions: ?Conditions, + }, + + pub fn toSecret( + conditions: SpendingConditions, + allocator: std.mem.Allocator, + ) !Nut10Secret { + switch (conditions) { + .p2pk => |condition| { + return Nut10Secret.init(allocator, .p2pk, &condition.data.toString(), v: { + if (condition.conditions) |c| break :v try c.toTags(allocator); + break :v null; + }); + }, + .htlc => |condition| { + return Nut10Secret.init(allocator, .htlc, &std.fmt.bytesToHex(condition.data, .lower), v: { + if (condition.conditions) |c| break :v try c.toTags(allocator); + break :v null; + }); + }, + } + } +}; + +/// P2PK and HTLC spending conditions +pub const Conditions = struct { + /// Unix locktime after which refund keys can be used + locktime: ?u64 = null, + /// Additional Public keys + pubkeys: ?std.ArrayList(secp256k1.PublicKey) = null, + /// Refund keys + refund_keys: ?std.ArrayList(secp256k1.PublicKey) = null, + /// Numbedr of signatures required + /// + /// Default is 1 + num_sigs: ?u64 = null, + /// Signature flag + /// + /// Default [`SigFlag.sig_inputs`] + sig_flag: SigFlag = .sig_inputs, + + pub fn deinit(self: Conditions) void { + if (self.pubkeys) |pk| pk.deinit(); + if (self.refund_keys) |rk| rk.deinit(); + } + + pub fn fromTags(_tags: []const []const []const u8, allocator: std.mem.Allocator) !Conditions { + var c = Conditions{}; + errdefer c.deinit(); + for (_tags) |at| { + const t = try Tag.fromSliceOfString(at, allocator); + switch (t) { + .pubkeys => |pk| c.pubkeys = pk, + .refund => |pk| c.refund_keys = pk, + .n_sigs => |n| c.num_sigs = n, + .sig_flag => |f| c.sig_flag = f, + .locktime => |n| c.locktime = n, + } + } + + return c; + } + + pub fn toTags(self: Conditions, allocator: std.mem.Allocator) !std.ArrayList(std.ArrayList(std.ArrayList(u8))) { + var res = try std.ArrayList(std.ArrayList(std.ArrayList(u8))).initCapacity(allocator, 5); + errdefer { + for (res.items) |it| { + for (it.items) |t| t.deinit(); + + it.deinit(); + } + res.deinit(); + } + + if (self.pubkeys) |pks| { + const t = Tag{ .pubkeys = pks }; + res.appendAssumeCapacity(try t.toSliceOfString(allocator)); + } + + if (self.refund_keys) |pks| { + const t = Tag{ .refund = pks }; + res.appendAssumeCapacity(try t.toSliceOfString(allocator)); + } + + if (self.locktime) |locktime| { + const t = Tag{ .locktime = locktime }; + res.appendAssumeCapacity(try t.toSliceOfString(allocator)); + } + + if (self.num_sigs) |num_sigs| { + const t = Tag{ .n_sigs = num_sigs }; + res.appendAssumeCapacity(try t.toSliceOfString(allocator)); + } + + const t = Tag{ .sig_flag = self.sig_flag }; + res.appendAssumeCapacity(try t.toSliceOfString(allocator)); + + return res; + } +}; + +/// Tag +pub const Tag = union(enum) { + /// sig_flag [`Tag`] + sig_flag: SigFlag, + /// Number of Sigs [`Tag`] + n_sigs: u64, + /// Locktime [`Tag`] + locktime: u64, + /// Refund [`Tag`] + refund: std.ArrayList(secp256k1.PublicKey), + /// Pubkeys [`Tag`] + pubkeys: std.ArrayList(secp256k1.PublicKey), + + pub fn deinit(self: Tag) void { + switch (self) { + .refund, .pubkeys => |t| t.deinit(), + else => {}, + } + } + /// Get [`Tag`] Kind + pub fn kind(self: Tag) TagKind { + return switch (self) { + .sig_flag => .sig_flag, + .n_sigs => .n_sigs, + .locktime => .locktime, + .refund => .refund, + .pubkeys => .pubkeys, + }; + } + + pub fn jsonStringify(self: @This(), out: anytype) !void { + try out.beginArray(); + try out.write(try self.kind().toString()); + switch (self) { + .sig_flag => |sig_flag| { + try out.write(sig_flag.toString()); + }, + .locktime, .n_sigs => |num| { + try out.write(num); + }, + .pubkeys, .refund => |pks| { + for (pks.items) |pk| { + try out.write(pk.toString()); + } + }, + } + try out.endArray(); + } + + pub fn toSliceOfString(self: Tag, allocator: std.mem.Allocator) !std.ArrayList(std.ArrayList(u8)) { + var res = try std.ArrayList(std.ArrayList(u8)).initCapacity(allocator, 2); + errdefer res.deinit(); + errdefer for (res.items) |r| r.deinit(); + + const kind_str = try self.kind().toString(); + // std.log.debug("to kind {s}", .{kind_str}); + var tag = try std.ArrayList(u8).initCapacity(allocator, kind_str.len); + tag.appendSliceAssumeCapacity(kind_str); + res.appendAssumeCapacity(tag); + + switch (self) { + .sig_flag => |sig_flag| { + const s = sig_flag.toString(); + var ss = try std.ArrayList(u8).initCapacity(allocator, s.len); + errdefer ss.deinit(); + ss.appendSliceAssumeCapacity(s); + + try res.append(ss); + }, + .locktime, .n_sigs => |num| { + var s = std.ArrayList(u8).init(allocator); + errdefer s.deinit(); + + try std.fmt.formatInt(num, 10, .lower, .{}, s.writer()); + + try res.append(s); + }, + .pubkeys, .refund => |pks| { + for (pks.items) |pk| { + var k = std.ArrayList(u8).init(allocator); + errdefer k.deinit(); + + try k.appendSlice(&(pk.toString())); + + try res.append(k); + } + }, + } + + return res; + } + + pub fn fromSliceOfString(tags: []const []const u8, allocator: std.mem.Allocator) !Tag { + if (tags.len == 0) return error.KindNotFound; + + const tag_kind = try TagKind.fromString(tags[0], allocator); + defer tag_kind.deinit(); + + // std.log.debug("tag_kind: {any}, from: {s}", .{ tag_kind, tags[0] }); + + return switch (tag_kind) { + .sig_flag => .{ .sig_flag = try SigFlag.fromString(tags[1]) }, + .n_sigs => .{ .n_sigs = try std.fmt.parseInt(u64, tags[1], 10) }, + .locktime => .{ .locktime = try std.fmt.parseInt(u64, tags[1], 10) }, + + .refund => v: { + var res = std.ArrayList(secp256k1.PublicKey).init(allocator); + errdefer res.deinit(); + + for (tags[1..]) |p| { + try res.append(try secp256k1.PublicKey.fromString(p)); + } + + break :v .{ .refund = res }; + }, + .pubkeys => v: { + var res = std.ArrayList(secp256k1.PublicKey).init(allocator); + errdefer res.deinit(); + + for (tags[1..]) |p| { + try res.append(try secp256k1.PublicKey.fromString(p)); + } + + break :v .{ .pubkeys = res }; + }, + + else => return error.UnknownTag, + }; + } +}; + +/// P2PK and HTLC Spending condition tags +pub const TagKind = union(enum) { + /// Signature flag + sig_flag, + /// Number signatures required + n_sigs, + /// Locktime + locktime, + /// Refund + refund, + /// Pubkey + pubkeys, + /// Custom tag kind + custom: std.ArrayList(u8), + + pub fn deinit(self: TagKind) void { + switch (self) { + .custom => |t| t.deinit(), + else => {}, + } + } + + pub fn fromString(tag: []const u8, allocator: std.mem.Allocator) !TagKind { + if (std.mem.eql(u8, tag, "sigflag")) return .sig_flag; + if (std.mem.eql(u8, tag, "n_sigs")) return .n_sigs; + if (std.mem.eql(u8, tag, "locktime")) return .locktime; + if (std.mem.eql(u8, tag, "refund")) return .refund; + if (std.mem.eql(u8, tag, "pubkeys")) return .pubkeys; + + var r = try std.ArrayList(u8).initCapacity(allocator, tag.len); + + r.appendSliceAssumeCapacity(tag); + + return .{ .custom = r }; + } + + pub fn toString(k: TagKind) ![]const u8 { + // std.log.debug("toString: {any}", .{k}); + return switch (k) { + .sig_flag => "sigflag", + .n_sigs => "n_sigs", + .locktime => "locktime", + .refund => "refund", + .pubkeys => "pubkeys", + .custom => |data| data.items, + }; + } +}; + +pub const SigFlag = enum { + /// Requires valid signatures on all inputs. + /// It is the default signature flag and will be applied even if the `sigflag` tag is absent. + sig_inputs, + /// Requires valid signatures on all inputs and on all outputs. + sig_all, + // TODO json decode + + pub fn fromString(tag: []const u8) !SigFlag { + if (std.mem.eql(u8, tag, "SIG_ALL")) { + return .sig_all; + } + + if (std.mem.eql(u8, tag, "SIG_INPUTS")) { + return .sig_inputs; + } + + return error.UnknownSigFlag; + } + + pub fn toString(f: SigFlag) []const u8 { + return switch (f) { + .sig_inputs => "SIG_INPUTS", + .sig_all => "SIG_ALL", + }; + } +}; + +/// Returns count of valid signatures +pub fn validSignatures(msg: []const u8, pubkeys: []const secp256k1.PublicKey, signatures: []const secp256k1.Signature) !u64 { + var count: usize = 0; + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + for (pubkeys) |pubkey| { + for (signatures) |signature| { + if (pubkey.verify(&secp, msg, signature)) { + count += 1; + } else |_| continue; + } + } + + return count; +} + +/// Sign [Proof] +pub fn signP2PKByProof(self: *Proof, allocator: std.mem.Allocator, secret_key: secp256k1.SecretKey) !void { + const msg = self.secret.toBytes(); + + const signature = try secret_key.sign(msg); + + if (self.witness) |*witness| { + try witness.addSignatures(allocator, &.{&signature.toString()}); + } else { + var p2pk_witness = Witness{ .p2pk_witness = .{ + .signatures = std.ArrayList(std.ArrayList(u8)).init(allocator), + } }; + errdefer p2pk_witness.deinit(); + + try p2pk_witness.addSignatures(allocator, &.{&signature.inner}); + + self.witness = p2pk_witness; + } +} + +/// Verify P2PK signature on [Proof] +pub fn verifyP2pkProof(self: *Proof, allocator: std.mem.Allocator) !void { + const secret = try Nut10Secret.fromSecret(self.secret, allocator); + defer secret.deinit(); + + const spending_conditions = if (secret.value.secret_data.tags) |tags| try Conditions.fromTags(tags, allocator) else Conditions{}; + defer spending_conditions.deinit(); + + const msg = self.secret.toBytes(); + + var valid_sigs: usize = 0; + + const witness_signatures = if (self.witness) |witness| witness.signatures() orelse return error.SignaturesNotProvided else return error.SignaturesNotProvided; + + var pubkeys: std.ArrayList(secp256k1.PublicKey) = if (spending_conditions.pubkeys) |pk| try pk.clone() else std.ArrayList(secp256k1.PublicKey).init(allocator); + defer pubkeys.deinit(); + + if (secret.value.kind == .p2pk) try pubkeys.append(try secp256k1.PublicKey.fromString(secret.value.secret_data.data)); + + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + for (witness_signatures.items) |signature| { + for (pubkeys.items) |v| { + const sig = try secp256k1.Signature.fromString(signature.items); + + if (v.verify(&secp, msg, sig)) { + valid_sigs += 1; + } else |_| { + std.log.debug("Could not verify signature: {any} on message: {any}", .{ + sig, + self.secret, + }); + } + } + } + + if (valid_sigs >= spending_conditions.num_sigs orelse 1) { + return; + } + + if (spending_conditions.locktime) |locktime| { + if (spending_conditions.refund_keys) |refund_keys| { + // If lock time has passed check if refund witness signature is valid + if (locktime < @as(u64, @intCast(std.time.timestamp()))) { + for (witness_signatures.items) |s| { + for (refund_keys.items) |v| { + const sig = secp256k1.Signature.fromString(s.items) catch return error.InvalidSignature; + // As long as there is one valid refund signature it can be spent + + if (v.verify(&secp, msg, sig)) { + return; + } else |_| {} + } + } + } + } + } + + return error.SpendConditionsNotMet; +} + +/// Sign [BlindedMessage] +pub fn signP2pkBlindedMessage(self: *BlindedMessage, allocator: std.mem.Allocator, secret_key: secp256k1.SecretKey) !void { + const msg = self.blinded_secret.serialize(); + const signature = try secret_key.sign(&msg); + + if (self.witness) |*witness| { + try witness.addSignatures(allocator, &.{&signature.inner}); + } else { + var p2pk_witness = Witness{ .p2pk_witness = .{ + .signatures = std.ArrayList(std.ArrayList(u8)).init(allocator), + } }; + errdefer p2pk_witness.deinit(); + + try p2pk_witness.addSignatures(allocator, &.{&signature.inner}); + self.witness = p2pk_witness; + } +} + +/// Verify P2PK conditions on [BlindedMessage] +pub fn verifyP2pkBlindedMessages(self: *const BlindedMessage, pubkeys: []const secp256k1.PublicKey, required_sigs: u64) !void { + var valid_sigs: usize = 0; + + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + if (self.witness) |witness| { + if (witness.signatures()) |signatures| { + for (signatures.items) |signature| { + for (pubkeys) |v| { + const msg = self.blinded_secret.serialize(); + const sig = try secp256k1.Signature.fromString(signature.items); + + if (v.verify(&secp, &msg, sig)) { + valid_sigs += 1; + } else |_| { + std.log.debug("Could not verify signature: {any} on message: {any}", .{ + sig, + self.blinded_secret, + }); + } + } + } + } else return error.SignaturesNotProvided; + } + + if (valid_sigs > required_sigs) return; + + return error.SpendConditionsNotMet; +} + +test "test_secret_ser" { + const data = try secp256k1.PublicKey.fromString( + "033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e", + ); + + var conditions = v: { + var pubkeys = std.ArrayList(secp256k1.PublicKey).init(std.testing.allocator); + errdefer pubkeys.deinit(); + + try pubkeys.append(try secp256k1.PublicKey.fromString("02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904")); + try pubkeys.append(try secp256k1.PublicKey.fromString("023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54")); + + var refund_keys = std.ArrayList(secp256k1.PublicKey).init(std.testing.allocator); + errdefer refund_keys.deinit(); + + try refund_keys.append(try secp256k1.PublicKey.fromString("033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e")); + + break :v Conditions{ + .locktime = 99999, + .pubkeys = pubkeys, + .refund_keys = refund_keys, + .num_sigs = 2, + .sig_flag = .sig_all, + }; + }; + defer conditions.deinit(); + + const tags = try conditions.toTags(std.testing.allocator); + defer { + for (tags.items) |t| { + for (t.items) |tt| { + tt.deinit(); + } + t.deinit(); + } + tags.deinit(); + } + + var secret: Nut10Secret = try Nut10Secret.init( + std.testing.allocator, + .p2pk, + &data.toString(), + tags, + ); + defer secret.deinit(); + + var secret_str = std.ArrayList(u8).init(std.testing.allocator); + defer secret_str.deinit(); + + try std.json.stringify(&secret, .{}, secret_str.writer()); + + const secret_der = try std.json.parseFromSlice(Nut10Secret, std.testing.allocator, secret_str.items, .{}); + defer secret_der.deinit(); + + try zul.testing.expectEqual(secret.kind, secret_der.value.kind); + + try zul.testing.expectEqualSlices(u8, secret.secret_data.nonce, secret_der.value.secret_data.nonce); + try zul.testing.expectEqualSlices(u8, secret.secret_data.data, secret_der.value.secret_data.data); + + try zul.testing.expectEqual(secret.secret_data.tags.?.len, secret_der.value.secret_data.tags.?.len); + + try zul.testing.expect(secret.secret_data.eql(secret_der.value.secret_data)); +} + +test "sign_proof" { + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + const secret_key = try secp256k1.SecretKey.fromString("99590802251e78ee1051648439eedb003dc539093a48a44e7b8f2642c909ea37"); + const secret_key_two = try secp256k1.SecretKey.fromString("0000000000000000000000000000000000000000000000000000000000000001"); + const secret_key_three = try secp256k1.SecretKey.fromString("7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f"); + + const v_key = secret_key.publicKey(secp); + const v_key_two = secret_key_two.publicKey(secp); + const v_key_three = secret_key_three.publicKey(secp); + + var pks = try std.ArrayList(secp256k1.PublicKey).initCapacity(std.testing.allocator, 2); + defer pks.deinit(); + + try pks.appendSlice(&.{ v_key_two, v_key_three }); + + var refund_keys = try std.ArrayList(secp256k1.PublicKey).initCapacity(std.testing.allocator, 1); + defer refund_keys.deinit(); + + try refund_keys.appendSlice(&.{v_key}); + + const conditions = Conditions{ + .locktime = 21000000000, + .pubkeys = pks, + .refund_keys = refund_keys, + .num_sigs = 2, + .sig_flag = .sig_inputs, + }; + + const tags = try conditions.toTags(std.testing.allocator); + defer { + for (tags.items) |tt| { + for (tt.items) |t| t.deinit(); + tt.deinit(); + } + + tags.deinit(); + } + + var secret: Nut10Secret = try Nut10Secret.init( + std.testing.allocator, + .p2pk, + &v_key.toString(), + tags, + ); + defer secret.deinit(); + + const nsecret = try secret.toSecret(std.testing.allocator); + + var proof = Proof{ + .keyset_id = try Id.fromStr("009a1f293253e41e"), + .amount = 0, + .secret = nsecret, + .c = try secp256k1.PublicKey.fromString("02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904"), + .witness = .{ .p2pk_witness = .{ .signatures = std.ArrayList(std.ArrayList(u8)).init(std.testing.allocator) } }, + }; + defer proof.deinit(std.testing.allocator); + + try signP2PKByProof(&proof, std.testing.allocator, secret_key); + try signP2PKByProof(&proof, std.testing.allocator, secret_key_two); + try verifyP2pkProof(&proof, std.testing.allocator); +} + +test "check conditions tags" { + var conditions = v: { + var pubkeys = std.ArrayList(secp256k1.PublicKey).init(std.testing.allocator); + errdefer pubkeys.deinit(); + + try pubkeys.append(try secp256k1.PublicKey.fromString("02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904")); + try pubkeys.append(try secp256k1.PublicKey.fromString("023192200a0cfd3867e48eb63b03ff599c7e46c8f4e41146b2d281173ca6c50c54")); + + var refund_keys = std.ArrayList(secp256k1.PublicKey).init(std.testing.allocator); + errdefer refund_keys.deinit(); + + try refund_keys.append(try secp256k1.PublicKey.fromString("033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e")); + + break :v Conditions{ + .locktime = 99999, + .pubkeys = pubkeys, + .refund_keys = refund_keys, + .num_sigs = 2, + .sig_flag = .sig_all, + }; + }; + defer conditions.deinit(); + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const tags = try conditions.toTags(arena.allocator()); + // std.log.debug("size of tags: {any}", .{tags.items.len}); + + const nconditions = try Conditions.fromTags(try helper.clone3dArrayToSlice(u8, arena.allocator(), tags), std.testing.allocator); + defer nconditions.deinit(); +} + +test "test_verify" { + // Proof with a valid signature + const json = + \\{ + \\ "amount":1, + \\ "secret":"[\"P2PK\",{\"nonce\":\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]", + \\ "C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904", + \\ "id":"009a1f293253e41e", + \\ "witness":"{\"signatures\":[\"60f3c9b766770b46caac1d27e1ae6b77c8866ebaeba0b9489fe6a15a837eaa6fcd6eaa825499c72ac342983983fd3ba3a8a41f56677cc99ffd73da68b59e1383\"]}" + \\} + ; + + var proof = try std.json.parseFromSlice(Proof, std.testing.allocator, json, .{}); + defer proof.deinit(); + + try verifyP2pkProof(&proof.value, std.testing.allocator); + + const invalid_json = + \\{"amount":1,"secret":"[\"P2PK\",{\"nonce\":\"859d4935c4907062a6297cf4e663e2835d90d97ecdd510745d32f6816323a41f\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","id":"009a1f293253e41e","witness":"{\"signatures\":[\"3426df9730d365a9d18d79bed2f3e78e9172d7107c55306ac5ddd1b2d065893366cfa24ff3c874ebf1fc22360ba5888ddf6ff5dbcb9e5f2f5a1368f7afc64f15\"]}"} + ; + + var invalid_proof = try std.json.parseFromSlice(Proof, std.testing.allocator, invalid_json, .{}); + defer invalid_proof.deinit(); + + try std.testing.expectError(error.SpendConditionsNotMet, verifyP2pkProof(&invalid_proof.value, std.testing.allocator)); +} + +test "verify_multi_sig" { + // Proof with 2 valid signatures to satifiy the condition + const json = + \\{"amount":0,"secret":"[\"P2PK\",{\"nonce\":\"0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"pubkeys\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\"],[\"n_sigs\",\"2\"],[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","id":"009a1f293253e41e","witness":"{\"signatures\":[\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\",\"9a72ca2d4d5075be5b511ee48dbc5e45f259bcf4a4e8bf18587f433098a9cd61ff9737dc6e8022de57c76560214c4568377792d4c2c6432886cc7050487a1f22\"]}"} + ; + + var proof = try std.json.parseFromSlice(Proof, std.testing.allocator, json, .{}); + defer proof.deinit(); + + try verifyP2pkProof(&proof.value, std.testing.allocator); + + // Proof with only one of the required signatures + const invalid_json = + \\{"amount":0,"secret":"[\"P2PK\",{\"nonce\":\"0ed3fcb22c649dd7bbbdcca36e0c52d4f0187dd3b6a19efcc2bfbebb5f85b2a1\",\"data\":\"0249098aa8b9d2fbec49ff8598feb17b592b986e62319a4fa488a3dc36387157a7\",\"tags\":[[\"pubkeys\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"02142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\"],[\"n_sigs\",\"2\"],[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","id":"009a1f293253e41e","witness":"{\"signatures\":[\"83564aca48c668f50d022a426ce0ed19d3a9bdcffeeaee0dc1e7ea7e98e9eff1840fcc821724f623468c94f72a8b0a7280fa9ef5a54a1b130ef3055217f467b3\"]}"} + ; + + var invalid_proof = try std.json.parseFromSlice(Proof, std.testing.allocator, invalid_json, .{}); + defer invalid_proof.deinit(); + + // Verification should fail without the requires signatures + try std.testing.expectError(error.SpendConditionsNotMet, verifyP2pkProof(&invalid_proof.value, std.testing.allocator)); +} + +test "verify_refund" { + const json = + \\{"amount":1,"id":"009a1f293253e41e","secret":"[\"P2PK\",{\"nonce\":\"902685f492ef3bb2ca35a47ddbba484a3365d143b9776d453947dcbf1ddf9689\",\"data\":\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\",\"tags\":[[\"pubkeys\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\"],[\"locktime\",\"21\"],[\"n_sigs\",\"2\"],[\"refund\",\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\"],[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","witness":"{\"signatures\":[\"710507b4bc202355c91ea3c147c0d0189c75e179d995e566336afd759cb342bcad9a593345f559d9b9e108ac2c9b5bd9f0b4b6a295028a98606a0a2e95eb54f7\"]}"} + ; + + var proof = try std.json.parseFromSlice(Proof, std.testing.allocator, json, .{}); + defer proof.deinit(); + + try verifyP2pkProof(&proof.value, std.testing.allocator); + + const invalid_json = + \\{"amount":1,"id":"009a1f293253e41e","secret":"[\"P2PK\",{\"nonce\":\"64c46e5d30df27286166814b71b5d69801704f23a7ad626b05688fbdb48dcc98\",\"data\":\"026f6a2b1d709dbca78124a9f30a742985f7eddd894e72f637f7085bf69b997b9a\",\"tags\":[[\"pubkeys\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\",\"03142715675faf8da1ecc4d51e0b9e539fa0d52fdd96ed60dbe99adb15d6b05ad9\"],[\"locktime\",\"21\"],[\"n_sigs\",\"2\"],[\"refund\",\"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\"],[\"sigflag\",\"SIG_INPUTS\"]]}]","C":"02698c4e2b5f9534cd0687d87513c759790cf829aa5739184a3e3735471fbda904","witness":"{\"signatures\":[\"f661d3dc046d636d47cb3d06586da42c498f0300373d1c2a4f417a44252cdf3809bce207c8888f934dba0d2b1671f1b8622d526840f2d5883e571b462630c1ff\"]}"} + ; + + var invalid_proof = try std.json.parseFromSlice(Proof, std.testing.allocator, invalid_json, .{}); + defer invalid_proof.deinit(); + + // Verification should fail without the requires signatures + try std.testing.expectError(error.SpendConditionsNotMet, verifyP2pkProof(&invalid_proof.value, std.testing.allocator)); +} diff --git a/src/core/nuts/nut12/nuts12.zig b/src/core/nuts/nut12/nuts12.zig new file mode 100644 index 0000000..0a5719a --- /dev/null +++ b/src/core/nuts/nut12/nuts12.zig @@ -0,0 +1,222 @@ +//! NUT-12: Offline ecash signature validation +//! +//! +const std = @import("std"); +const secp256k1 = @import("../../secp256k1.zig"); +const dhke = @import("../../dhke.zig"); +const Proof = @import("../nut00/lib.zig").Proof; +const BlindSignature = @import("../nut00/lib.zig").BlindSignature; +const Id = @import("../nut02/nut02.zig").Id; + +/// Blinded Signature on Dleq +/// +/// Defined in [NUT12](https://github.com/cashubtc/nuts/blob/main/12.md) +pub const BlindSignatureDleq = struct { + /// e + e: secp256k1.SecretKey, + /// s + s: secp256k1.SecretKey, + + // pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + // const value = try std.json.innerParse([]const u8, allocator, source, options); + // std.log.warn("ssss {s}", .{value}); + // return undefined; + // } +}; + +/// Proof Dleq +/// +/// Defined in [NUT12](https://github.com/cashubtc/nuts/blob/main/12.md) +pub const ProofDleq = struct { + /// e + e: secp256k1.SecretKey, + /// s + s: secp256k1.SecretKey, + /// Blinding factor + r: secp256k1.SecretKey, +}; + +/// Verify DLEQ +fn verifyDleq( + blinded_message: secp256k1.PublicKey, // B' + blinded_signature: secp256k1.PublicKey, // C' + _e: secp256k1.SecretKey, + _s: secp256k1.SecretKey, + mint_pubkey: secp256k1.PublicKey, // A +) !void { + const e_bytes: [32]u8 = _e.data; + const e: secp256k1.Scalar = secp256k1.Scalar.fromSecretKey(_e); + + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + // a = e*A + var a = try mint_pubkey.mulTweak(&secp, e); + + // R1 = s*G - a + a = a.negate(&secp); + const r1 = try _s.publicKey(secp).combine(a); // s*G + (-a) + + // b = s*B' + const s = secp256k1.Scalar.fromSecretKey(_s); + const b = try blinded_message.mulTweak(&secp, s); + + // c = e*C' + var c = try blinded_signature.mulTweak(&secp, e); + + // R2 = b - c + c = c.negate(&secp); + const r2 = try b.combine(c); + + // hash(R1,R2,A,C') + const hash_e = dhke.hashE(&.{ r1, r2, mint_pubkey, blinded_signature }); + + if (!std.meta.eql(e_bytes, hash_e)) { + std.log.warn("DLEQ on signature failed", .{}); + std.log.warn("e_bytes: {any}, hash_e: {any}", .{ e_bytes, hash_e }); + return error.InvalidDleqProof; + } +} + +fn calculateDleq( + secp: secp256k1.Secp256k1, + blinded_signature: secp256k1.PublicKey, // C' + blinded_message: secp256k1.PublicKey, // B' + mint_secret_key: secp256k1.SecretKey, // a +) !BlindSignatureDleq { + // Random nonce + const r = secp256k1.SecretKey.generate(); + + // R1 = r*G + const r1 = r.publicKey(secp); + + // R2 = r*B' + const r_scal = secp256k1.Scalar.fromSecretKey(r); + + const r2 = try blinded_message.mulTweak(&secp, r_scal); + + // e = hash(R1,R2,A,C') + const e = dhke.hashE(&.{ r1, r2, mint_secret_key.publicKey(secp), blinded_signature }); + const e_sk = try secp256k1.SecretKey.fromSlice(&e); + + // s1 = e*a + const s1 = try e_sk.mulTweak(secp256k1.Scalar.fromSecretKey(mint_secret_key)); + + // s = r + s1 + const s = try r.addTweak(secp256k1.Scalar.fromSecretKey(s1)); + + return .{ + .e = e_sk, + .s = s, + }; +} + +/// Verify proof Dleq by [`Proof`] +pub fn verifyDleqByProof(self: *const Proof, secp: secp256k1.Secp256k1, mint_pubkey: secp256k1.PublicKey) !void { + if (self.dleq) |dleq| { + const y = try dhke.hashToCurve(self.secret.inner); + + const r = secp256k1.Scalar.fromSecretKey(dleq.r); + const bs1 = try mint_pubkey.mulTweak(&secp, r); + + const blinded_signature = try self.c.combine(bs1); + const blinded_message = try y.combine(dleq.r.publicKey(secp)); + + return try verifyDleq( + blinded_message, + blinded_signature, + dleq.e, + dleq.s, + mint_pubkey, + ); + } + + return error.MissingDleqProof; +} + +/// Verify dleq on proof by [`BlindSignature`] +pub inline fn verifyDleqByBlindSignature( + self: BlindSignature, + mint_pubkey: secp256k1.PublicKey, + blinded_message: secp256k1.PublicKey, +) !void { + if (self.dleq) |dleq| { + return try verifyDleq(blinded_message, self.c, dleq.e, dleq.s, mint_pubkey); + } + + return error.MissingDleqProof; +} + +/// Add Dleq to proof for [`BlindSignature`] +/// r = random nonce +/// R1 = r*G +/// R2 = r*B' +/// e = hash(R1,R2,A,C') +/// s = r + e*a +pub fn addDleqProofByBlindSignature( + self: *BlindSignature, + secp: secp256k1.Secp256k1, + blinded_message: secp256k1.PublicKey, + mint_secretkey: secp256k1.SecretKey, +) !void { + const dleq = try calculateDleq(secp, self.c, blinded_message, mint_secretkey); + self.dleq = dleq; +} + +/// New DLEQ for [`BlindSignature`] +pub inline fn initBlindSignature( + secp: secp256k1.Secp256k1, + amount: u64, + blinded_signature: secp256k1.PublicKey, + keyset_id: Id, + blinded_message: secp256k1.PublicKey, + mint_secretkey: secp256k1.SecretKey, +) !BlindSignature { + return .{ + .amount = amount, + .keyset_id = keyset_id, + .c = blinded_signature, + .dleq = try calculateDleq(secp, blinded_signature, blinded_message, mint_secretkey), + }; +} + +test "test_blind_signature_dleq" { + const blinded_sig = + \\{"amount":8,"id":"00882760bfa2eb41","C_":"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2","dleq":{"e":"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73d9","s":"9818e061ee51d5c8edc3342369a554998ff7b4381c8652d724cdf46429be73da"}} + ; + + const blinded = try std.json.parseFromSlice(BlindSignature, std.testing.allocator, blinded_sig, .{}); + defer blinded.deinit(); + + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + const secret_key = + try secp256k1.SecretKey.fromString("0000000000000000000000000000000000000000000000000000000000000001"); + + const mint_key = secret_key.publicKey(secp); + + const blinded_secret = try secp256k1.PublicKey.fromString( + "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2", + ); + + try verifyDleqByBlindSignature(blinded.value, mint_key, blinded_secret); +} + +test "test_proof_dleq" { + const proof_json = + \\{"amount": 1,"id": "00882760bfa2eb41","secret": "daf4dd00a2b68a0858a80450f52c8a7d2ccf87d375e43e216e0c571f089f63e9","C": "024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc","dleq": {"e": "b31e58ac6527f34975ffab13e70a48b6d2b0d35abc4b03f0151f09ee1a9763d4","s": "8fbae004c59e754d71df67e392b6ae4e29293113ddc2ec86592a0431d16306d8","r": "a6d13fcd7a18442e6076f5e1e7c887ad5de40a019824bdfa9fe740d302e8d861"}} + ; + + const proof = try std.json.parseFromSlice(Proof, std.testing.allocator, proof_json, .{}); + defer proof.deinit(); + + // A + const a = try secp256k1.PublicKey.fromString( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ); + const secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + try verifyDleqByProof(&proof.value, secp, a); +} diff --git a/src/core/nuts/nut13/nut13.zig b/src/core/nuts/nut13/nut13.zig new file mode 100644 index 0000000..4b9d3fa --- /dev/null +++ b/src/core/nuts/nut13/nut13.zig @@ -0,0 +1,270 @@ +//! NUT-13: Deterministic Secrets +//! +//! + +const secp256k1 = @import("../../secp256k1.zig"); +const SecretKey = secp256k1.SecretKey; +const Secret = @import("../../secret.zig").Secret; +const Id = @import("../nut02/nut02.zig").Id; +const nut00 = @import("../nut00/lib.zig"); +const BlindedMessage = nut00.BlindedMessage; +const PreMint = nut00.PreMint; +const PreMintSecrets = nut00.PreMintSecrets; +const bip32 = @import("../../bip32/bip32.zig"); +const std = @import("std"); +const amount_lib = @import("../../amount.zig"); +const dhke = @import("../../dhke.zig"); +const helper = @import("../../../helper/helper.zig"); +const bip39 = @import("../../bip39/bip39.zig"); + +fn derivePathFromKeysetId(id: Id) ![3]bip32.ChildNumber { + const index: u32 = @intCast(try id.toU64() % ((std.math.powi(u64, 2, 31) catch unreachable) - 1)); + + const keyset_child_number = try bip32.ChildNumber.fromHardenedIdx(index); + + return .{ + try bip32.ChildNumber.fromHardenedIdx(129372), + try bip32.ChildNumber.fromHardenedIdx(0), + keyset_child_number, + }; +} + +/// Create new [`Secret`] from xpriv +/// allocating result, need to call deinit with this allocator +pub fn secretFromXpriv(allocator: std.mem.Allocator, secp: secp256k1.Secp256k1, xpriv: bip32.ExtendedPrivKey, keyset_id: Id, counter: u32) !Secret { + const path = (try derivePathFromKeysetId(keyset_id)) ++ [_]bip32.ChildNumber{ + try bip32.ChildNumber.fromHardenedIdx(counter), + try bip32.ChildNumber.fromNormalIdx(0), + }; + + const derived_xpriv = try xpriv.derivePriv(secp, &path); + + const data = try allocator.alloc(u8, 64); + errdefer allocator.free(data); + + @memcpy(data, &std.fmt.bytesToHex(derived_xpriv.private_key.secretBytes(), .lower)); + + return .{ + .inner = data, + }; +} + +/// Create new [`SecretKey`] from xpriv +pub fn secretKeyFromXpriv(secp: secp256k1.Secp256k1, xpriv: bip32.ExtendedPrivKey, keyset_id: Id, counter: u32) !SecretKey { + const path = (try derivePathFromKeysetId(keyset_id)) ++ [_]bip32.ChildNumber{ + try bip32.ChildNumber.fromHardenedIdx(counter), + try bip32.ChildNumber.fromNormalIdx(1), + }; + const derived_xpriv = try xpriv.derivePriv(secp, &path); + + return derived_xpriv.private_key; +} + +/// Generate blinded messages from predetermined secrets and blindings +/// factor +pub fn preMintSecretsFromXpriv( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + keyset_id: Id, + _counter: u32, + xpriv: bip32.ExtendedPrivKey, + amount: amount_lib.Amount, + amount_split_target: amount_lib.SplitTarget, +) !helper.Parsed(PreMintSecrets) { + var pre_mint_secrets = try helper.Parsed(PreMintSecrets).init(allocator); + errdefer pre_mint_secrets.deinit(); + + pre_mint_secrets.value.keyset_id = keyset_id; + + var counter = _counter; + const splitted = try amount_lib.splitTargeted(amount, allocator, amount_split_target); + defer splitted.deinit(); + + var secrets = std.ArrayList(PreMint).init(pre_mint_secrets.arena.allocator()); + defer secrets.deinit(); + + for (splitted.items) |amnt| { + const secret = try secretFromXpriv(pre_mint_secrets.arena.allocator(), secp, xpriv, keyset_id, counter); + defer secret.deinit(allocator); + + const blinding_factor = try secretKeyFromXpriv(secp, xpriv, keyset_id, counter); + + const blinded, const r = try dhke.blindMessage(secp, secret.toBytes(), blinding_factor); + + const blinded_message = nut00.BlindedMessage{ + .amount = amnt, + .keyset_id = keyset_id, + .blinded_secret = blinded, + }; + + try secrets.append(.{ + .blinded_message = blinded_message, + .secret = secret, + .r = r, + .amount = amnt, + }); + + counter += 1; + } + + pre_mint_secrets.value.secrets = try secrets.toOwnedSlice(); + + return pre_mint_secrets; +} + +/// New [`PreMintSecrets`] from xpriv with a zero amount used for change +pub fn preMintSecretsFromXprivBlank( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + keyset_id: Id, + _counter: u32, + xpriv: bip32.ExtendedPrivKey, + amount: amount_lib.Amount, +) !helper.Parsed(PreMintSecrets) { + var pre_mint_secrets = try helper.Parsed(PreMintSecrets).init(allocator); + errdefer pre_mint_secrets.deinit(); + + pre_mint_secrets.value.keyset_id = keyset_id; + + if (amount <= 0) { + return pre_mint_secrets; + } + + const count = @max(@as(u64, 1), @as(u64, @intFromFloat(std.math.ceil(std.math.log2(@as(f64, @floatFromInt(amount))))))); + + var counter = _counter; + + var secrets = std.ArrayList(PreMint).init(pre_mint_secrets.arena.allocator()); + defer secrets.deinit(); + + for (0..count) |_| { + const secret = try secretFromXpriv(pre_mint_secrets.arena.allocator(), secp, xpriv, keyset_id, counter); + + const blinding_factor = try secretKeyFromXpriv(secp, xpriv, keyset_id, counter); + + const blinded, const r = try dhke.blindMessage(secp, secret.toBytes(), blinding_factor); + + const amnt: amount_lib.Amount = 0; + + const blinded_message = BlindedMessage{ + .amount = amnt, + .keyset_id = keyset_id, + .blinded_secret = blinded, + }; + + try secrets.append(.{ + .blinded_message = blinded_message, + .secret = secret, + .r = r, + .amount = amnt, + }); + + counter += 1; + } + + pre_mint_secrets.value.secrets = try secrets.toOwnedSlice(); + return pre_mint_secrets; +} + +/// Generate blinded messages from predetermined secrets and blindings factor +pub fn preMintSecretsRestoreBatch( + allocator: std.mem.Allocator, + secp: secp256k1.Secp256k1, + keyset_id: Id, + xpriv: bip32.ExtendedPrivKey, + start_count: u32, + end_count: u32, +) !helper.Parsed(PreMintSecrets) { + var pre_mint_secrets = try helper.Parsed(PreMintSecrets).init(allocator); + errdefer pre_mint_secrets.deinit(); + + var secrets = std.ArrayList(PreMint).init(pre_mint_secrets.arena.allocator()); + defer secrets.deinit(); + + for (start_count..end_count + 1) |i| { + const secret = try secretFromXpriv(pre_mint_secrets.arena.allocator(), secp, xpriv, keyset_id, @truncate(i)); + + const blinding_factor = try secretKeyFromXpriv(secp, xpriv, keyset_id, @truncate(i)); + + const blinded, const r = try dhke.blindMessage(secp, secret.toBytes(), blinding_factor); + + const blinded_message = BlindedMessage{ + .amount = 0, + .keyset_id = keyset_id, + .blinded_secret = blinded, + }; + + try secrets.append(.{ + .blinded_message = blinded_message, + .secret = secret, + .r = r, + .amount = 0, + }); + } + + pre_mint_secrets.value.secrets = try secrets.toOwnedSlice(); + + return pre_mint_secrets; +} + +test "test_secret_from_seed" { + const seed_words = + "half depart obvious quality work element tank gorilla view sugar picture humble"; + + const mnemonic = try bip39.Mnemonic.parseInNormalized(.english, seed_words); + + const seed = try mnemonic.toSeedNormalized(""); + + const xpriv = try bip32.ExtendedPrivKey.initMaster(.MAINNET, &seed); + + const keyset_id = try Id.fromStr("009a1f293253e41e"); + + const test_secrets = [_][]const u8{ + "485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae", + "8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270", + "bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8", + "59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf", + "576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0", + }; + + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + for (0.., test_secrets) |i, test_secret| { + const secret = try secretFromXpriv(std.testing.allocator, secp, xpriv, keyset_id, @truncate(i)); + defer secret.deinit(std.testing.allocator); + + try std.testing.expectEqualSlices(u8, test_secret, secret.inner); + } +} + +test "test_r_from_seed" { + const seed_words = + "half depart obvious quality work element tank gorilla view sugar picture humble"; + + const mnemonic = try bip39.Mnemonic.parseInNormalized(.english, seed_words); + + const seed = try mnemonic.toSeedNormalized(""); + + const xpriv = try bip32.ExtendedPrivKey.initMaster(.MAINNET, &seed); + + const keyset_id = try Id.fromStr("009a1f293253e41e"); + + const test_secrets = [_][]const u8{ + "ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679", + "967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248", + "b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899", + "fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29", + "5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9", + }; + + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + for (0.., test_secrets) |i, test_secret| { + const sk = try secretKeyFromXpriv(secp, xpriv, keyset_id, @truncate(i)); + + const expected_sk = try SecretKey.fromString(test_secret); + try std.testing.expectEqualDeep(expected_sk, sk); + } +} diff --git a/src/core/nuts/nut14/nut14.zig b/src/core/nuts/nut14/nut14.zig new file mode 100644 index 0000000..ce327ee --- /dev/null +++ b/src/core/nuts/nut14/nut14.zig @@ -0,0 +1,113 @@ +//! NUT-14: Hashed Time Lock Contacts (HTLC) +//! +//! +const std = @import("std"); +const Witness = @import("../nut00/lib.zig").Witness; +const Proof = @import("../nut00/lib.zig").Proof; +const Secret = @import("../nut10/nut10.zig").Secret; +const Conditions = @import("../nut11/nut11.zig").Conditions; +const secp256k1 = @import("../../secp256k1.zig"); +const Signature = secp256k1.Signature; + +const validSignatures = @import("../nut11/nut11.zig").validSignatures; + +/// HTLC Witness +pub const HTLCWitness = struct { + /// Primage + preimage: std.ArrayList(u8), + /// Signatures + signatures: ?std.ArrayList(std.ArrayList(u8)), + + pub fn deinit(self: HTLCWitness) void { + if (self.signatures) |s| { + for (s.items) |ss| ss.deinit(); + s.deinit(); + } + + self.preimage.deinit(); + } +}; + +/// Verify HTLC +pub fn verifyHTLC(self: *const Proof, allocator: std.mem.Allocator) !void { + var secret = try Secret.fromSecret( + self.secret, + allocator, + ); + defer secret.deinit(); + + const conditions: ?Conditions = v: { + break :v Conditions.fromTags(secret.value.secret_data.tags orelse break :v null, allocator) catch null; + }; + defer if (conditions) |c| c.deinit(); + + const htlc_witness: HTLCWitness = if (self.witness) |witness| v: { + break :v switch (witness) { + .htlc_witness => |w| w, + else => return error.IncorrectSecretKind, + }; + } else return error.IncorrectSecretKind; + + if (conditions) |conds| { + // Check locktime + if (conds.locktime) |locktime| { + // If locktime is in passed and no refund keys provided anyone can spend + if (locktime < @as(u64, @intCast(std.time.timestamp())) and conds.refund_keys == null) { + return; + } + + // If refund keys are provided verify p2pk signatures + if (conds.refund_keys) |refund_key| { + if (self.witness) |signatures| { + const signs = signatures.signatures() orelse return error.SignaturesNotProvided; + var signs_parsed = try std.ArrayList(Signature).initCapacity(allocator, signs.items.len); + defer signs_parsed.deinit(); + + for (signs.items) |s| { + signs_parsed.appendAssumeCapacity(try Signature.fromString(s.items)); + } + + // If secret includes refund keys check that there is a valid signature + if (try validSignatures(self.secret.toBytes(), refund_key.items, signs_parsed.items) >= 1) return; + } + } + } + + // If pubkeys are present check there is a valid signature + if (conds.pubkeys) |pubkey| { + const req_sigs = conds.num_sigs orelse 1; + + const signs = htlc_witness.signatures orelse return error.SignaturesNotProvided; + + var signatures = try std.ArrayList(Signature).initCapacity(allocator, signs.items.len); + defer signatures.deinit(); + + for (signs.items) |s| { + signatures.appendAssumeCapacity(try Signature.fromString(s.items)); + } + + if (try validSignatures(self.secret.toBytes(), pubkey.items, signatures.items) < req_sigs) return error.IncorrectSecretKind; + } + } + + if (secret.value.kind != .htlc) { + return error.IncorrectSecretKind; + } + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + hasher.update(secret.value.secret_data.data); + const hash_lock = hasher.finalResult(); + hasher = std.crypto.hash.sha2.Sha256.init(.{}); + hasher.update(htlc_witness.preimage.items); + + const preimage_hash = hasher.finalResult(); + + if (!std.meta.eql(hash_lock, preimage_hash)) return error.Preimage; +} + +/// Add Preimage +pub fn addPreimage(self: *Proof, preimage: std.ArrayList(u8)) void { + self.witness = .{ .htlc_witness = .{ + .preimage = preimage, + .signatures = null, + } }; +} diff --git a/src/core/nuts/nut15/nut15.zig b/src/core/nuts/nut15/nut15.zig new file mode 100644 index 0000000..3c69725 --- /dev/null +++ b/src/core/nuts/nut15/nut15.zig @@ -0,0 +1,30 @@ +//! NUT-15: Multipart payments +//! +//! + +const CurrencyUnit = @import("../nut00/lib.zig").CurrencyUnit; +const PaymentMethod = @import("../nut00/lib.zig").PaymentMethod; + +const std = @import("std"); + +/// Multi-part payment +pub const Mpp = struct { + /// Amount + amount: u64, +}; + +/// Mpp Method Settings +pub const MppMethodSettings = struct { + /// Payment Method e.g. bolt11 + method: PaymentMethod = .bolt11, + /// Currency Unit e.g. sat + unit: CurrencyUnit = .sat, + /// Multi part payment support + mpp: bool = false, +}; + +/// Mpp Settings +pub const Settings = struct { + /// Method settings + methods: []const MppMethodSettings = &.{}, +}; diff --git a/src/core/primitives.zig b/src/core/primitives.zig deleted file mode 100644 index efff240..0000000 --- a/src/core/primitives.zig +++ /dev/null @@ -1,57 +0,0 @@ -const std = @import("std"); -const PublicKey = @import("secp256k1.zig").PublicKey; -const keyset = @import("keyset.zig"); -const Proof = @import("proof.zig").Proof; -const blind = @import("blind.zig"); -const helper = @import("../helper/helper.zig"); - -pub const CurrencyUnit = enum(u8) { - sat, - msat, - usd, -}; - -pub const KeysResponse = struct { - keysets: []const KeyResponse, - - pub fn initFrom(keysets: []const KeyResponse) KeysResponse { - return .{ - .keysets = keysets, - }; - } - - // pub fn deinit(self: @This()) void { - // self.keysets.deinit(); - // } -}; - -pub const KeyResponse = struct { - id: [16]u8, - unit: CurrencyUnit, - keys: std.AutoHashMap(u64, PublicKey), - - pub fn jsonStringify(self: @This(), out: anytype) !void { - try out.beginObject(); - - try out.objectField("id"); - try out.write(self.id); - - try out.objectField("unit"); - try out.write(self.unit); - - try out.objectField("keys"); - try keyset.stringifyMapOfPubkeysWriter(out, self.keys); - - try out.endObject(); - } -}; - -pub const PostSwapRequest = struct { - inputs: helper.JsonArrayList(Proof), - outputs: helper.JsonArrayList(blind.BlindedMessage), - - pub fn deinit(self: @This()) void { - self.inputs.value.deinit(); - self.outputs.value.deinit(); - } -}; diff --git a/src/core/secp256k1.zig b/src/core/secp256k1.zig index b2989ab..492f18e 100644 --- a/src/core/secp256k1.zig +++ b/src/core/secp256k1.zig @@ -1,28 +1,122 @@ const std = @import("std"); + const secp256k1 = @cImport({ @cInclude("secp256k1.h"); @cInclude("secp256k1_recovery.h"); @cInclude("secp256k1_preallocated.h"); + @cInclude("secp256k1_schnorrsig.h"); }); const crypto = std.crypto; +pub const KeyPair = struct { + inner: secp256k1.secp256k1_keypair, + + /// Creates a [`KeyPair`] directly from a Secp256k1 secret key. + pub fn fromSecretKey(secp: *const Secp256k1, sk: *const SecretKey) !KeyPair { + var kp = secp256k1.secp256k1_keypair{}; + + if (secp256k1.secp256k1_keypair_create(secp.ctx, &kp, &sk.data) != 1) { + @panic("the provided secret key is invalid: it is corrupted or was not produced by Secp256k1 library"); + } + + return .{ .inner = kp }; + } +}; + +pub const XOnlyPublicKey = struct { + inner: secp256k1.secp256k1_xonly_pubkey, + + /// Creates a schnorr public key directly from a slice. + /// + /// # Errors + /// + /// Returns [`Error::InvalidPublicKey`] if the length of the data slice is not 32 bytes or the + /// slice does not represent a valid Secp256k1 point x coordinate. + pub inline fn fromSlice(data: []const u8) !XOnlyPublicKey { + if (data.len == 0 or data.len != 32) { + return error.InvalidPublicKey; + } + + var pk: secp256k1.secp256k1_xonly_pubkey = undefined; + + if (secp256k1.secp256k1_xonly_pubkey_parse( + secp256k1.secp256k1_context_no_precomp, + &pk, + data.ptr, + ) == 1) { + return .{ .inner = pk }; + } + + return error.InvalidPublicKey; + } + + /// Serializes the key as a byte-encoded x coordinate value (32 bytes). + pub inline fn serialize(self: XOnlyPublicKey) [32]u8 { + var ret: [32]u8 = undefined; + + const err = secp256k1.secp256k1_xonly_pubkey_serialize( + secp256k1.secp256k1_context_no_precomp, + &ret, + &self.inner, + ); + std.debug.assert(err == 1); + return ret; + } + + /// Creates a [`PublicKey`] using the key material from `pk` combined with the `parity`. + pub fn publicKey(pk: XOnlyPublicKey, parity: enum { + even, + odd, + }) !PublicKey { + var buf: [33]u8 = undefined; + + // First byte of a compressed key should be `0x02 AND parity`. + buf[0] = switch (parity) { + .even => 0x02, + .odd => 0x03, + }; + + buf[1..33].* = pk.serialize(); + + return PublicKey.fromSlice(&buf) catch @panic("buffer is valid"); + } +}; + pub const Secp256k1 = struct { ctx: ?*secp256k1.struct_secp256k1_context_struct, - ptr: []align(16) u8, - pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { - allocator.free(@as([]align(16) u8, @ptrCast(self.ptr))); + pub fn deinit(self: @This()) void { + secp256k1.secp256k1_context_preallocated_destroy(self.ctx); + } + + /// Verifies a schnorr signature. + pub fn verifySchnorr( + self: *Secp256k1, + sig: Signature, + msg: [32]u8, + pubkey: XOnlyPublicKey, + ) !void { + if (secp256k1.secp256k1_schnorrsig_verify( + self.ctx, + &sig.inner, + &msg, + 32, + &pubkey.inner, + ) != 1) return error.InvalidSignature; } - pub fn genNew(allocator: std.mem.Allocator) !@This() { - // verify and sign only - const size: usize = secp256k1.secp256k1_context_preallocated_size(257 | 513); + pub fn signSchnorrHelper(self: *const Secp256k1, msg: [32]u8, keypair: KeyPair, nonce_data: []const u8) !Signature { + var sig: [64]u8 = undefined; + + std.debug.assert(1 == secp256k1.secp256k1_schnorrsig_sign(self.ctx, (&sig).ptr, &msg, &keypair.inner, nonce_data.ptr)); - const ptr = try allocator.alignedAlloc(u8, 16, size); + return .{ .inner = sig }; + } + pub fn genNew() !@This() { const ctx = - secp256k1.secp256k1_context_preallocated_create(ptr.ptr, 257 | 513); + secp256k1.secp256k1_context_create(257 | 513); var seed: [32]u8 = undefined; @@ -34,11 +128,66 @@ pub const Secp256k1 = struct { return .{ .ctx = ctx, - .ptr = ptr, }; } }; +/// A tag used for recovering the public key from a compact signature. +pub const RecoveryId = struct { + value: i32, + + /// Allows library users to create valid recovery IDs from i32. + pub fn fromI32(id: i32) !RecoveryId { + return switch (id) { + 0...3 => .{ .value = id }, + else => error.InvalidRecoveryId, + }; + } + + pub fn toI32(self: RecoveryId) i32 { + return self.value; + } +}; + +/// An ECDSA signature with a recovery ID for pubkey recovery. +pub const RecoverableSignature = struct { + sig: secp256k1.secp256k1_ecdsa_recoverable_signature, + + /// Converts a compact-encoded byte slice to a signature. This + /// representation is nonstandard and defined by the libsecp256k1 library. + pub fn fromCompact(data: []const u8, recid: RecoveryId) !RecoverableSignature { + if (data.len == 0) { + return error.InvalidSignature; + } + + var ret = secp256k1.secp256k1_ecdsa_recoverable_signature{}; + + if (data.len != 64) { + return error.InvalidSignature; + } else if (secp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact( + secp256k1.secp256k1_context_no_precomp, + &ret, + data.ptr, + recid.value, + ) == 1) { + return .{ .sig = ret }; + } else { + return error.InvalidSignature; + } + } + + /// Serializes the recoverable signature in compact format. + pub fn serializeCompact(self: RecoverableSignature) !struct { RecoveryId, [64]u8 } { + var ret = [_]u8{0} ** 64; + var recid: i32 = 0; + + const err = secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact(secp256k1.secp256k1_context_no_precomp, &ret, &recid, &self.sig); + std.debug.assert(err == 1); + + return .{ .{ .value = recid }, ret }; + } +}; + pub const Scalar = struct { data: [32]u8, @@ -50,6 +199,10 @@ pub const Scalar = struct { pub const PublicKey = struct { pk: secp256k1.secp256k1_pubkey, + pub fn eql(self: PublicKey, other: PublicKey) bool { + return std.mem.eql(u8, &self.pk.data, &other.pk.data); + } + // json serializing func pub fn jsonStringify(self: PublicKey, out: anytype) !void { try out.write(std.fmt.bytesToHex(&self.serialize(), .lower)); @@ -68,6 +221,44 @@ pub const PublicKey = struct { } } + /// Returns the [`XOnlyPublicKey`] (and it's [`Parity`]) for this [`PublicKey`]. + pub inline fn xOnlyPublicKey(self: *const PublicKey) !struct { XOnlyPublicKey, enum { even, odd } } { + var pk_parity: i32 = 0; + var xonly_pk = secp256k1.secp256k1_xonly_pubkey{}; + const ret = secp256k1.secp256k1_xonly_pubkey_from_pubkey( + secp256k1.secp256k1_context_no_precomp, + &xonly_pk, + &pk_parity, + &self.pk, + ); + + std.debug.assert(ret == 1); + + return .{ + .{ .inner = xonly_pk }, + if (pk_parity & 1 == 0) .even else .odd, + }; + } + + /// Verify schnorr signature + pub fn verify(self: *const PublicKey, secp: *Secp256k1, msg: []const u8, sig: Signature) !void { + var hasher = crypto.hash.sha2.Sha256.init(.{}); + hasher.update(msg); + + const hash = hasher.finalResult(); + + try secp.verifySchnorr(sig, hash, (try self.xOnlyPublicKey())[0]); + } + + /// [`PublicKey`] from hex string + pub fn fromString(s: []const u8) !@This() { + var buf: [100]u8 = undefined; + const decoded = try std.fmt.hexToBytes(&buf, s); + + return PublicKey.fromSlice(decoded); + } + + /// [`PublicKey`] from bytes slice pub fn fromSlice(c: []const u8) !@This() { var pk: secp256k1.secp256k1_pubkey = .{}; @@ -96,6 +287,14 @@ pub const PublicKey = struct { return ret; } + /// Serializes the key as a byte-encoded pair of values, in uncompressed form. + pub inline fn serializeUncompressed(self: PublicKey) [65]u8 { + var ret = [_]u8{0} ** 65; + + self.serializeInternal(&ret, 2); + return ret; + } + inline fn serializeInternal(self: PublicKey, ret: []u8, flag: u32) void { var ret_len = ret.len; @@ -121,6 +320,24 @@ pub const PublicKey = struct { return error.InvalidTweak; } + /// Tweaks a [`PublicKey`] by adding `tweak * G` modulo the curve order. + /// + /// # Errors + /// + /// Returns an error if the resulting key would be invalid. + pub inline fn addExpTweak( + self: *const PublicKey, + secp: Secp256k1, + tweak: Scalar, + ) !PublicKey { + var s = self.*; + if (secp256k1.secp256k1_ec_pubkey_tweak_add(secp.ctx, &s.pk, &tweak.data) == 1) { + return s; + } else { + return error.InvalidTweak; + } + } + pub fn combine(self: @This(), other: PublicKey) !PublicKey { return PublicKey.combineKeys(&.{ &self, &other, @@ -144,9 +361,61 @@ pub const PublicKey = struct { } }; +pub const Signature = struct { + inner: [64]u8, + + pub fn fromString(s: []const u8) !Signature { + if (s.len / 2 > 64) return error.InvalidSignature; + var res = [_]u8{0} ** 64; + + _ = try std.fmt.hexToBytes(&res, s); + return .{ .inner = res }; + } + + pub fn toString(self: Signature) [128]u8 { + return std.fmt.bytesToHex(&self.inner, .lower); + } +}; + pub const SecretKey = struct { data: [32]u8, + /// Generate random [`SecretKey`] + pub fn generate() SecretKey { + var rng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp())); + + var d: [32]u8 = undefined; + + while (true) { + rng.fill(&d); + if (SecretKey.fromSlice(&d)) |sk| return sk else |_| continue; + } + } + + /// Schnorr Signature on Message + pub fn sign(self: *const SecretKey, msg: []const u8) !Signature { + var hasher = crypto.hash.sha2.Sha256.init(.{}); + hasher.update(msg); + + const hash = hasher.finalResult(); + + var secp = try Secp256k1.genNew(); + defer secp.deinit(); + + var rng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp())); + + var aux: [32]u8 = undefined; + rng.fill(&aux); + + return secp.signSchnorrHelper(hash, try KeyPair.fromSecretKey(&secp, self), &aux); + } + + pub fn fromString(data: []const u8) !@This() { + var buf: [100]u8 = undefined; + + return SecretKey.fromSlice(try std.fmt.hexToBytes(&buf, data)); + } + pub fn fromSlice(data: []const u8) !@This() { if (data.len != 32) { return error.InvalidSecretKey; @@ -173,4 +442,51 @@ pub const SecretKey = struct { pub fn toString(self: @This()) [32 * 2]u8 { return std.fmt.bytesToHex(&self.data, .lower); } + + /// Tweaks a [`SecretKey`] by multiplying by `tweak` modulo the curve order. + /// + /// # Errors + /// + /// Returns an error if the resulting key would be invalid. + pub inline fn mulTweak(self: *const SecretKey, tweak: Scalar) !SecretKey { + var s = self.*; + if (secp256k1.secp256k1_ec_seckey_tweak_mul( + secp256k1.secp256k1_context_no_precomp, + &s.data, + &tweak.data, + ) != 1) { + return error.InvalidTweak; + } + + return s; + } + + /// Tweaks a [`SecretKey`] by adding `tweak` modulo the curve order. + /// + /// # Errors + /// + /// Returns an error if the resulting key would be invalid. + pub inline fn addTweak(self: *const SecretKey, tweak: Scalar) !SecretKey { + var s = self.*; + if (secp256k1.secp256k1_ec_seckey_tweak_add( + secp256k1.secp256k1_context_no_precomp, + &s.data, + &tweak.data, + ) != 1) { + return error.InvalidTweak; + } else { + return s; + } + } + + pub fn jsonStringify(self: *const SecretKey, out: anytype) !void { + try out.write(self.toString()); + } + + pub fn jsonParse(_: std.mem.Allocator, source: anytype, _: std.json.ParseOptions) !@This() { + return switch (try source.next()) { + .string, .allocated_string => |hex_sec| SecretKey.fromString(hex_sec) catch return error.UnexpectedToken, + else => return error.UnexpectedToken, + }; + } }; diff --git a/src/core/secret.zig b/src/core/secret.zig new file mode 100644 index 0000000..e23e168 --- /dev/null +++ b/src/core/secret.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const zul = @import("zul"); + +pub const Secret = struct { + inner: []const u8, + + pub fn clone(self: Secret, allocator: std.mem.Allocator) !Secret { + const data = try allocator.alloc(u8, self.inner.len); + + @memcpy(data, self.inner); + return .{ + .inner = data, + }; + } + + pub fn deinit(self: Secret, allocator: std.mem.Allocator) void { + allocator.free(self.inner); + } + + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { + return .{ .inner = try std.json.innerParse([]const u8, allocator, source, options) }; + } + + /// Create secret value + /// Generate a new random secret as the recommended 32 byte hex + pub fn generate(allocator: std.mem.Allocator) !Secret { + var random_bytes: [32]u8 = undefined; + + var rng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp())); + + // Generate random bytes + rng.fill(&random_bytes); + + var result = std.ArrayList(u8).init(allocator); + errdefer result.deinit(); + + try std.fmt.format(result.writer(), "{s}", .{std.fmt.fmtSliceHexLower(&random_bytes)}); + + // The secret string is hex encoded + return .{ + .inner = try result.toOwnedSlice(), + }; + } + + pub fn toBytes(self: Secret) []const u8 { + return self.inner; + } +}; diff --git a/src/helper/helper.zig b/src/helper/helper.zig index b180743..484d4ba 100644 --- a/src/helper/helper.zig +++ b/src/helper/helper.zig @@ -1,5 +1,105 @@ const std = @import("std"); +pub fn Parsed(comptime T: type) type { + return struct { + arena: *std.heap.ArenaAllocator, + value: T, + + pub fn init(allocator: std.mem.Allocator) !@This() { + var parsed = Parsed(T){ + .arena = try allocator.create(std.heap.ArenaAllocator), + .value = undefined, + }; + errdefer allocator.destroy(parsed.arena); + + parsed.arena.* = std.heap.ArenaAllocator.init(allocator); + errdefer parsed.arena.deinit(); + + return parsed; + } + + pub fn deinit(self: @This()) void { + const allocator = self.arena.child_allocator; + self.arena.deinit(); + allocator.destroy(self.arena); + } + }; +} + +pub fn clone2dArrayToSlice(comptime T: type, allocator: std.mem.Allocator, array: std.ArrayList(std.ArrayList(T))) ![]const []const T { + var result = try allocator.alloc([]const T, array.items.len); + errdefer { + for (result) |r| allocator.free(r); + allocator.free(result); + } + + for (0.., array.items) |idx, arr| { + const slice = try allocator.alloc(T, arr.items.len); + + @memcpy(slice, arr.items); + result[idx] = slice; + } + + return result; +} + +pub fn clone3dArrayToSlice( + comptime T: type, + allocator: std.mem.Allocator, + arr: std.ArrayList(std.ArrayList(std.ArrayList(T))), +) ![]const []const []const T { + var result = try allocator.alloc([]const []const T, arr.items.len); + errdefer { + for (result) |rr| { + for (rr) |rrr| allocator.free(rrr); + allocator.free(rr); + } + + allocator.free(result); + } + + for (0.., arr.items) |idx, ar| { + result[idx] = try clone2dArrayToSlice(T, allocator, ar); + } + + return result; +} + +pub fn clone3dSliceToArrayList(comptime T: type, allocator: std.mem.Allocator, slice: []const []const []const T) !std.ArrayList(std.ArrayList(std.ArrayList(T))) { + var result = try std.ArrayList(std.ArrayList(std.ArrayList(T))).initCapacity(allocator, slice.len); + errdefer { + for (result.items) |r| { + for (r.items) |rr| rr.deinit(); + r.deinit(); + } + + result.deinit(); + } + + for (slice) |item| { + result.appendAssumeCapacity(try clone2dSliceToArrayList(T, allocator, item)); + } + + return result; +} + +pub fn clone2dSliceToArrayList(comptime T: type, allocator: std.mem.Allocator, slice: []const []const T) !std.ArrayList(std.ArrayList(T)) { + var result = try std.ArrayList(std.ArrayList(T)).initCapacity(allocator, slice.len); + errdefer { + for (result.items) |r| r.deinit(); + } + + for (slice) |item| { + var sl = try std.ArrayList(T).initCapacity(allocator, item.len); + + sl.appendSliceAssumeCapacity(item); + + result.appendAssumeCapacity(sl); + } + + return result; +} + pub fn JsonArrayList(comptime T: type) type { return struct { const Self = @This(); @@ -7,7 +107,6 @@ pub fn JsonArrayList(comptime T: type) type { value: std.ArrayList(T), pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !Self { - errdefer std.log.err("ssssaaaa", .{}); if (try source.next() != .array_begin) return error.UnexpectedToken; var result = std.ArrayList(T).init(allocator); @@ -68,6 +167,8 @@ pub fn RenameJsonField(comptime T: type, comptime field_from_to: std.StaticStrin var res: T = undefined; + var fields_seen = [_]bool{false} ** full_map.len; + while (true) { // taking token name from source const name_token: ?std.json.Token = try source.nextAllocMax(allocator, .alloc_if_needed, options.max_value_len.?); @@ -83,10 +184,11 @@ pub fn RenameJsonField(comptime T: type, comptime field_from_to: std.StaticStrin var f = false; - inline for (full_map) |p| { + inline for (0.., full_map) |i, p| { if (std.mem.eql(u8, p[1], field_name)) { @field(&res, p[0]) = try std.json.innerParse(fieldType(T, p[0]).?, allocator, source, options); f = true; + fields_seen[i] = true; break; } } @@ -97,6 +199,21 @@ pub fn RenameJsonField(comptime T: type, comptime field_from_to: std.StaticStrin } } + for (0.., fields_seen) |i, seen| { + if (!seen) { + inline for (field.fields) |f| { + if (std.mem.eql(u8, f.name, full_map[i][0])) { + if (f.default_value) |default_ptr| { + const default = @as(*align(1) const f.type, @ptrCast(default_ptr)).*; + @field(&res, f.name) = default; + } else { + return error.MissingField; + } + } + } + } + } + return res; } @@ -106,12 +223,30 @@ pub fn RenameJsonField(comptime T: type, comptime field_from_to: std.StaticStrin switch (@typeInfo(@TypeOf(self))) { .Pointer => |p| { switch (@typeInfo(p.child)) { - .Struct => |s| { - inline for (s.fields) |f| { - const rename_to = comptime ffto.get(f.name).?; - try out.objectField(rename_to); - try out.write(@field(self, f.name)); + .Struct => |S| { + inline for (S.fields) |Field| { + // don't include void fields + if (Field.type == void) continue; + + var emit_field = true; + + // don't include optional fields that are null when emit_null_optional_fields is set to false + if (@typeInfo(Field.type) == .Optional) { + if (out.options.emit_null_optional_fields == false) { + if (@field(self, Field.name) == null) { + emit_field = false; + } + } + } + + if (emit_field) { + try out.objectField(ffto.get(Field.name) orelse return error.OutOfMemory); + } + try out.write(@field(self, Field.name)); } + + try out.endObject(); + return; }, else => { @compileError("expect type Struct, got: " ++ @typeName(p.child)); diff --git a/src/lib.zig b/src/lib.zig index 0a01416..467eba8 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,6 +1,12 @@ -pub usingnamespace @import("core/lib.zig"); -pub usingnamespace @import("mint/database/database.zig"); +const std = @import("std"); +pub const core = @import("core/lib.zig"); +// pub usingnamespace @import("core/lib.zig"); +// TODO return after fix mint +// pub usingnamespace @import("mint/lib.zig"); +// pub const bech32 = @import("bech32/bech32.zig"); test { - @import("std").testing.refAllDeclsRecursive(@This()); + std.testing.log_level = .warn; + std.testing.refAllDeclsRecursive(@This()); + // std.testing.refAllDeclsRecursive(bech32); } diff --git a/src/main.zig b/src/main.zig index e320564..2d61d28 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,5 @@ const std = @import("std"); const cli = @import("zig-cli"); -const bdhke = @import("core/bdhke.zig"); // Configuration settings for the CLI const Args = struct { @@ -65,9 +64,6 @@ fn execute() !void { } fn runInfo(allocator: std.mem.Allocator, _cfg: Args) !void { - const dhke = try bdhke.Dhke.init(allocator); - defer dhke.deinit(); - const stdout = std.io.getStdOut().writer(); try stdout.print("Version: 0.1.0\n", .{}); diff --git a/src/mint.zig b/src/mint.zig index d565fe2..902b554 100644 --- a/src/mint.zig +++ b/src/mint.zig @@ -1,18 +1 @@ -const std = @import("std"); -const mint = @import("mint/lib.zig"); -const core = @import("core/lib.zig"); - -pub fn main() !void { - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); - const allocator = arena.allocator(); - - // read mint conifg from env // args - // - const cfg = try mint.config.MintConfig.readConfigWithDefaults(allocator); - - var m = try mint.Mint.init(allocator, cfg); - defer m.deinit(); - - try mint.server.runServer(allocator, &m); -} +pub fn main() void {} diff --git a/src/mint/config.zig b/src/mint/config.zig deleted file mode 100644 index fc866cb..0000000 --- a/src/mint/config.zig +++ /dev/null @@ -1,33 +0,0 @@ -const std = @import("std"); - -pub const MintConfig = struct { - privatekey: []const u8, - derivation_path: ?[]const u8 = null, - server: ServerConfig = .{}, - - database: DatabaseConfig = .{ - // TODO: i think we need to split it to another entity in main - .db_url = "some-db-configuration", - }, - - pub fn readConfigWithDefaults(_: std.mem.Allocator) !MintConfig { - // TODO read from cli - // so we here need to read configuration - return .{ - .privatekey = "my_private_key", - }; - } -}; - -pub const ServerConfig = struct { - host: []const u8 = "0.0.0.0", - port: u16 = 3338, - serve_wallet_path: ?[]const u8 = null, - api_prefix: ?[]const u8 = null, -}; - -pub const DatabaseConfig = struct { - db_url: []const u8, - - max_connections: u32 = 5, -}; diff --git a/src/mint/database/database.zig b/src/mint/database/database.zig index fb9c173..384b55f 100644 --- a/src/mint/database/database.zig +++ b/src/mint/database/database.zig @@ -1,6 +1,9 @@ const std = @import("std"); const core = @import("../../core/lib.zig"); +const model = @import("../model.zig"); +const zul = @import("zul"); +// TODO remove deinit from Database and Tx(?) pub const Tx = struct { // These two fields are the same as before ptr: *anyopaque, @@ -61,6 +64,22 @@ pub const Database = struct { addUsedProofsFn: *const fn (ptr: *anyopaque, tx: Tx, proofs: []const core.proof.Proof) anyerror!void, getUsedProofsFn: *const fn (ptr: *anyopaque, tx: Tx, allocator: std.mem.Allocator) anyerror![]core.proof.Proof, + addPendingInvoiceFn: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator, tx: Tx, key: []const u8, invoice: model.Invoice) anyerror!void, + getPendingInvoiceFn: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator, tx: Tx, key: []const u8) anyerror!model.Invoice, + deletePendingInvoiceFn: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator, tx: Tx, key: []const u8) anyerror!void, + + getBolt11MintQuoteFn: *const fn (ptr: *anyopaque, _: std.mem.Allocator, _: Tx, id: zul.UUID) anyerror!core.primitives.Bolt11MintQuote, + + updateBolt11MintQuoteFn: *const fn (ptr: *anyopaque, _: std.mem.Allocator, _: Tx, quote: core.primitives.Bolt11MintQuote) anyerror!void, + + addBolt11MintQuoteFn: *const fn (ptr: *anyopaque, _: std.mem.Allocator, _: Tx, _: core.primitives.Bolt11MintQuote) anyerror!void, + + getBolt11MeltQuoteFn: *const fn (ptr: *anyopaque, _: std.mem.Allocator, _: Tx, id: zul.UUID) anyerror!core.primitives.Bolt11MeltQuote, + + updateBolt11MeltQuoteFn: *const fn (ptr: *anyopaque, _: std.mem.Allocator, _: Tx, quote: core.primitives.Bolt11MeltQuote) anyerror!void, + + addBolt11MeltQuoteFn: *const fn (ptr: *anyopaque, _: std.mem.Allocator, _: Tx, _: core.primitives.Bolt11MeltQuote) anyerror!void, + // This is new fn init(ptr: anytype) Database { const T = @TypeOf(ptr); @@ -86,6 +105,60 @@ pub const Database = struct { const self: T = @ptrCast(@alignCast(pointer)); return ptr_info.Pointer.child.getUsedProofs(self, tx, allocator); } + + pub fn addPendingInvoice(pointer: *anyopaque, allocator: std.mem.Allocator, tx: Tx, key: []const u8, invoice: model.Invoice) anyerror!void { + const self: T = @ptrCast(@alignCast(pointer)); + + return ptr_info.Pointer.child.addPendingInvoice(self, allocator, tx, key, invoice); + } + + pub fn getPendingInvoice(pointer: *anyopaque, allocator: std.mem.Allocator, tx: Tx, key: []const u8) anyerror!model.Invoice { + const self: T = @ptrCast(@alignCast(pointer)); + + return ptr_info.Pointer.child.getPendingInvoice(self, allocator, tx, key); + } + + pub fn deletePendingInvoice(pointer: *anyopaque, allocator: std.mem.Allocator, tx: Tx, key: []const u8) anyerror!void { + const self: T = @ptrCast(@alignCast(pointer)); + + return ptr_info.Pointer.child.deletePendingInvoice(self, allocator, tx, key); + } + + pub fn updateBolt11MintQuote(pointer: *anyopaque, allocator: std.mem.Allocator, tx: Tx, quote: core.primitives.Bolt11MintQuote) anyerror!void { + const self: T = @ptrCast(@alignCast(pointer)); + + return ptr_info.Pointer.child.updateBolt11MintQuote(self, allocator, tx, quote); + } + + pub fn getBolt11MintQuote(pointer: *anyopaque, allocator: std.mem.Allocator, tx: Tx, id: zul.UUID) !core.primitives.Bolt11MintQuote { + const self: T = @ptrCast(@alignCast(pointer)); + + return ptr_info.Pointer.child.getBolt11MintQuote(self, allocator, tx, id); + } + + pub fn addBolt11MintQuote(pointer: *anyopaque, allocator: std.mem.Allocator, tx: Tx, quote: core.primitives.Bolt11MintQuote) !void { + const self: T = @ptrCast(@alignCast(pointer)); + + return ptr_info.Pointer.child.addBolt11MintQuote(self, allocator, tx, quote); + } + + pub fn updateBolt11MeltQuote(pointer: *anyopaque, allocator: std.mem.Allocator, tx: Tx, quote: core.primitives.Bolt11MeltQuote) anyerror!void { + const self: T = @ptrCast(@alignCast(pointer)); + + return ptr_info.Pointer.child.updateBolt11MeltQuote(self, allocator, tx, quote); + } + + pub fn getBolt11MeltQuote(pointer: *anyopaque, allocator: std.mem.Allocator, tx: Tx, id: zul.UUID) !core.primitives.Bolt11MeltQuote { + const self: T = @ptrCast(@alignCast(pointer)); + + return ptr_info.Pointer.child.getBolt11MeltQuote(self, allocator, tx, id); + } + + pub fn addBolt11MeltQuote(pointer: *anyopaque, allocator: std.mem.Allocator, tx: Tx, quote: core.primitives.Bolt11MeltQuote) !void { + const self: T = @ptrCast(@alignCast(pointer)); + + return ptr_info.Pointer.child.addBolt11MeltQuote(self, allocator, tx, quote); + } }; return .{ @@ -94,6 +167,16 @@ pub const Database = struct { .deinitFn = gen.deinit, .addUsedProofsFn = gen.addUsedProofs, .getUsedProofsFn = gen.getUsedProofs, + .addPendingInvoiceFn = gen.addPendingInvoice, + .getPendingInvoiceFn = gen.getPendingInvoice, + .deletePendingInvoiceFn = gen.deletePendingInvoice, + .updateBolt11MintQuoteFn = gen.updateBolt11MintQuote, + .getBolt11MintQuoteFn = gen.getBolt11MintQuote, + .addBolt11MintQuoteFn = gen.addBolt11MintQuote, + + .updateBolt11MeltQuoteFn = gen.updateBolt11MeltQuote, + .getBolt11MeltQuoteFn = gen.getBolt11MeltQuote, + .addBolt11MeltQuoteFn = gen.addBolt11MeltQuote, }; } @@ -110,6 +193,42 @@ pub const Database = struct { return self.getUsedProofsFn(self.ptr, tx, allocator); } + pub fn addPendingInvoice(self: Database, allocator: std.mem.Allocator, tx: Tx, key: []const u8, invoice: model.Invoice) !void { + return self.addPendingInvoiceFn(self.ptr, allocator, tx, key, invoice); + } + + pub fn getPendingInvoice(self: Database, allocator: std.mem.Allocator, tx: Tx, key: []const u8) !model.Invoice { + return self.getPendingInvoiceFn(self.ptr, allocator, tx, key); + } + + pub fn deletePendingInvoice(self: Database, allocator: std.mem.Allocator, tx: Tx, key: []const u8) !void { + return self.deletePendingInvoiceFn(self.ptr, allocator, tx, key); + } + + pub fn getBolt11MintQuote(self: Database, allocator: std.mem.Allocator, tx: Tx, id: zul.UUID) !core.primitives.Bolt11MintQuote { + return self.getBolt11MintQuoteFn(self.ptr, allocator, tx, id); + } + + pub fn updateBolt11MintQuote(self: Database, allocator: std.mem.Allocator, tx: Tx, quote: core.primitives.Bolt11MintQuote) !void { + return self.updateBolt11MintQuoteFn(self.ptr, allocator, tx, quote); + } + + pub fn addBolt11MintQuote(self: Database, allocator: std.mem.Allocator, tx: Tx, quote: core.primitives.Bolt11MintQuote) !void { + return self.addBolt11MintQuoteFn(self.ptr, allocator, tx, quote); + } + + pub fn getBolt11MeltQuote(self: Database, allocator: std.mem.Allocator, tx: Tx, id: zul.UUID) !core.primitives.Bolt11MeltQuote { + return self.getBolt11MeltQuoteFn(self.ptr, allocator, tx, id); + } + + pub fn updateBolt11MeltQuote(self: Database, allocator: std.mem.Allocator, tx: Tx, quote: core.primitives.Bolt11MeltQuote) !void { + return self.updateBolt11MeltQuoteFn(self.ptr, allocator, tx, quote); + } + + pub fn addBolt11MeltQuote(self: Database, allocator: std.mem.Allocator, tx: Tx, quote: core.primitives.Bolt11MeltQuote) !void { + return self.addBolt11MeltQuoteFn(self.ptr, allocator, tx, quote); + } + pub fn deinit(self: Database) void { return self.deinitFn(self.ptr); } @@ -142,8 +261,195 @@ pub const InMemory = struct { } }; + const Bolt11MintQuotes = struct { + const Bolt11MintQuote = struct { + id: zul.UUID, + payment_request: []const u8, + expiry: u64, + paid: bool, + + pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { + allocator.free(self.payment_request); + } + }; + + quotes: std.ArrayList(Bolt11MintQuote), + allocator: std.mem.Allocator, + + fn init(allocator: std.mem.Allocator) !Bolt11MintQuotes { + return .{ + .quotes = std.ArrayList(Bolt11MintQuote).init(allocator), + .allocator = allocator, + }; + } + + fn deinit(self: @This()) void { + for (self.quotes.items) |q| q.deinit(); + } + + fn update(self: *@This(), quote: core.primitives.Bolt11MintQuote) !void { + if (self.getPtr(quote.quote_id)) |q| { + const q_old = q.*; + const q_cloned = try quote.clone(self.allocator); + + q.* = Bolt11MintQuote{ + .id = q_cloned.quote_id, + .payment_request = q_cloned.payment_request, + .expiry = q_cloned.expiry, + .paid = q_cloned.paid, + }; + q_old.deinit(self.allocator); + } else return error.QuoteNotFound; + } + + fn add(self: *@This(), quote: core.primitives.Bolt11MintQuote) !void { + if (self.get(quote.quote_id) != null) return error.QuoteAlreadyExist; + + const payment_request = try self.allocator.alloc(u8, quote.payment_request.len); + errdefer self.allocator.free(payment_request); + + try self.quotes.append(.{ + .id = quote.quote_id, + .payment_request = payment_request, + .expiry = quote.expiry, + .paid = quote.paid, + }); + } + + fn get(self: *@This(), id: zul.UUID) ?Bolt11MintQuote { + for (self.quotes.items) |i| if (id.eql(i.id)) return i; + + return null; + } + + fn getPtr(self: *@This(), id: zul.UUID) ?*Bolt11MintQuote { + for (self.quotes.items) |*i| if (id.eql(i.id)) return i; + + return null; + } + + fn delete(self: *@This(), id: zul.UUID) !void { + for (0.., self.quotes.items) |idx, i| { + if (i.eql(id)) { + self.quotes.orderedRemove(idx); + return; + } + } + } + }; + + const Bolt11MeltQuotes = struct { + quotes: std.ArrayList(core.primitives.Bolt11MeltQuote), + allocator: std.mem.Allocator, + + fn init(allocator: std.mem.Allocator) !Bolt11MeltQuotes { + return .{ + .quotes = std.ArrayList(core.primitives.Bolt11MeltQuote).init(allocator), + .allocator = allocator, + }; + } + + fn deinit(self: @This()) void { + for (self.quotes.items) |q| q.deinit(); + } + + fn update(self: *@This(), quote: core.primitives.Bolt11MeltQuote) !void { + if (self.getPtr(quote.quote_id)) |q| { + const q_old = q.*; + q.* = try quote.clone(self.allocator); + q_old.deinit(self.allocator); + } else return error.QuoteNotFound; + } + + fn add(self: *@This(), quote: core.primitives.Bolt11MeltQuote) !void { + if (self.get(quote.quote_id) != null) return error.QuoteAlreadyExist; + + const new = try quote.clone(self.allocator); + errdefer new.deinit(self.allocator); + + try self.quotes.append(new); + } + + fn get(self: *@This(), id: zul.UUID) ?core.primitives.Bolt11MeltQuote { + for (self.quotes.items) |i| if (id.eql(i.quote_id)) return i; + + return null; + } + + fn getPtr(self: *@This(), id: zul.UUID) ?*core.primitives.Bolt11MeltQuote { + for (self.quotes.items) |*i| if (id.eql(i.quote_id)) return i; + + return null; + } + + fn delete(self: *@This(), id: zul.UUID) !void { + for (0.., self.quotes.items) |idx, i| { + if (i.quote_id.eql(id)) { + self.quotes.orderedRemove(idx); + return; + } + } + } + }; + + const PendingInvoices = struct { + const PendingInvoice = struct { + key: []const u8, + amount: u64, + payment_request: []const u8, + + fn deinit(self: PendingInvoice, allocator: std.mem.Allocator) void { + allocator.free(self.key); + allocator.free(self.payment_request); + } + }; + + invoices: std.ArrayList(PendingInvoice), + allocator: std.mem.Allocator, + + fn deinit(self: PendingInvoices) void { + for (self.invoices.items) |i| i.deinit(); + } + + fn init(allocator: std.mem.Allocator) !PendingInvoices { + return .{ + .invoices = std.ArrayList(PendingInvoice).init(allocator), + .allocator = allocator, + }; + } + + fn add(self: *@This(), invoice: PendingInvoice) !void { + if (self.get(invoice.key) != null) return error.InvoiceDuplicate; + + const key = try self.allocator.alloc(u8, invoice.key.len); + errdefer self.allocator.free(key); + + const payment_request = try self.allocator.alloc(u8, invoice.payment_request.len); + errdefer self.allocator.free(payment_request); + + try self.invoices.append(.{ .key = key, .payment_request = payment_request, .amount = invoice.amount }); + } + + fn get(self: *@This(), key: []const u8) ?PendingInvoice { + for (self.invoices.items) |i| if (std.mem.eql(u8, i.key, key)) return i; + return null; + } + + fn delete(self: *@This(), key: []const u8) !void { + for (0.., self.invoices.items) |idx, i| { + if (std.mem.eql(u8, i.key, key)) { + _ = self.invoices.orderedRemove(idx); + return; + } + } + } + }; + allocator: std.mem.Allocator, proofs: std.ArrayList(core.proof.Proof), + pending_invoices: PendingInvoices, + mint_quotes: Bolt11MintQuotes, + melt_quotes: Bolt11MeltQuotes, pub fn init(allocator: std.mem.Allocator) !Database { var self = try allocator.create(Self); @@ -152,6 +458,15 @@ pub const InMemory = struct { self.allocator = allocator; self.proofs = std.ArrayList(core.proof.Proof).init(allocator); + self.pending_invoices = try PendingInvoices.init(allocator); + errdefer self.pending_invoices.deinit(); + + self.mint_quotes = try Bolt11MintQuotes.init(allocator); + errdefer self.mint_quotes.deinit(); + + self.melt_quotes = try Bolt11MeltQuotes.init(allocator); + errdefer self.melt_quotes.deinit(); + return Database.init(self); } @@ -180,6 +495,57 @@ pub const InMemory = struct { return res; } + + pub fn addPendingInvoice(self: *Self, _: std.mem.Allocator, _: Tx, key: []const u8, invoice: model.Invoice) anyerror!void { + try self.pending_invoices.add(.{ .key = key, .amount = invoice.amount, .payment_request = invoice.payment_request }); + } + + pub fn getPendingInvoice(self: *Self, allocator: std.mem.Allocator, _: Tx, key: []const u8) !model.Invoice { + const invoice = self.pending_invoices.get(key) orelse return error.PendingInvoiceNotFound; + + const inv = model.Invoice{ .amount = invoice.amount, .payment_request = invoice.payment_request }; + + return inv.clone(allocator); + } + + pub fn deletePendingInvoice(self: *Self, _: std.mem.Allocator, _: Tx, key: []const u8) anyerror!void { + return try self.pending_invoices.delete(key); + } + + pub fn getBolt11MintQuote(self: *Self, allocator: std.mem.Allocator, _: Tx, id: zul.UUID) !core.primitives.Bolt11MintQuote { + const q = self.mint_quotes.get(id) orelse return error.NotFound; + + const qq = core.primitives.Bolt11MintQuote{ + .quote_id = q.id, + .payment_request = q.payment_request, + .expiry = q.expiry, + .paid = q.paid, + }; + + return qq.clone(allocator); + } + + pub fn addBolt11MintQuote(self: *Self, _: std.mem.Allocator, _: Tx, quote: core.primitives.Bolt11MintQuote) !void { + return self.mint_quotes.add(quote); + } + + pub fn updateBolt11MintQuote(self: *Self, _: std.mem.Allocator, _: Tx, quote: core.primitives.Bolt11MintQuote) !void { + return self.mint_quotes.update(quote); + } + + pub fn getBolt11MeltQuote(self: *Self, allocator: std.mem.Allocator, _: Tx, id: zul.UUID) !core.primitives.Bolt11MeltQuote { + const q = self.melt_quotes.get(id) orelse return error.NotFound; + + return q.clone(allocator); + } + + pub fn addBolt11MeltQuote(self: *Self, _: std.mem.Allocator, _: Tx, quote: core.primitives.Bolt11MeltQuote) !void { + return self.melt_quotes.add(quote); + } + + pub fn updateBolt11MeltQuote(self: *Self, _: std.mem.Allocator, _: Tx, quote: core.primitives.Bolt11MeltQuote) !void { + return self.melt_quotes.update(quote); + } }; test "dfd" { diff --git a/src/mint/lib.zig b/src/mint/lib.zig deleted file mode 100644 index 9f447a5..0000000 --- a/src/mint/lib.zig +++ /dev/null @@ -1,4 +0,0 @@ -pub const config = @import("config.zig"); -pub const server = @import("server.zig"); -pub const routes = @import("routes/lib.zig"); -pub usingnamespace @import("mint.zig"); diff --git a/src/mint/lightning/invoices/constants.zig b/src/mint/lightning/invoices/constants.zig new file mode 100644 index 0000000..288a5ff --- /dev/null +++ b/src/mint/lightning/invoices/constants.zig @@ -0,0 +1,12 @@ +/// Tag constants as specified in BOLT11 +pub const TAG_PAYMENT_HASH: u8 = 1; +pub const TAG_DESCRIPTION: u8 = 13; +pub const TAG_PAYEE_PUB_KEY: u8 = 19; +pub const TAG_DESCRIPTION_HASH: u8 = 23; +pub const TAG_EXPIRY_TIME: u8 = 6; +pub const TAG_MIN_FINAL_CLTV_EXPIRY_DELTA: u8 = 24; +pub const TAG_FALLBACK: u8 = 9; +pub const TAG_PRIVATE_ROUTE: u8 = 3; +pub const TAG_PAYMENT_SECRET: u8 = 16; +pub const TAG_PAYMENT_METADATA: u8 = 27; +pub const TAG_FEATURES: u8 = 5; diff --git a/src/mint/lightning/invoices/error.zig b/src/mint/lightning/invoices/error.zig new file mode 100644 index 0000000..e7febe3 --- /dev/null +++ b/src/mint/lightning/invoices/error.zig @@ -0,0 +1,25 @@ +const std = @import("std"); + +pub const Bolt11ParseError = error{ + // Bech32Error(bech32::Error), + ParseAmountError, + MalformedSignature, + BadPrefix, + UnknownCurrency, + UnknownSiPrefix, + MalformedHRP, + TooShortDataPart, + UnexpectedEndOfTaggedFields, + DescriptionDecodeError, + PaddingError, + IntegerOverflowError, + InvalidSegWitProgramLength, + InvalidPubKeyHashLength, + InvalidScriptHashLength, + InvalidRecoveryId, + InvalidSliceLength, + + /// Not an error, but used internally to signal that a part of the invoice should be ignored + /// according to BOLT11 + Skip, +}; diff --git a/src/mint/lightning/invoices/invoice.zig b/src/mint/lightning/invoices/invoice.zig new file mode 100644 index 0000000..d4a17be --- /dev/null +++ b/src/mint/lightning/invoices/invoice.zig @@ -0,0 +1,654 @@ +const std = @import("std"); +const errors = @import("error.zig"); +const core = @import("../../../core/lib.zig"); +const constants = @import("constants.zig"); +const bech32 = @import("../../../bech32/bech32.zig"); + +/// Construct the invoice's HRP and signatureless data into a preimage to be hashed. +pub fn constructInvoicePreimage(allocator: std.mem.Allocator, hrp_bytes: []const u8, data_without_signature: []const u5) !std.ArrayList(u8) { + var preimage = try std.ArrayList(u8).initCapacity(allocator, hrp_bytes.len); + errdefer preimage.deinit(); + + preimage.appendSliceAssumeCapacity(hrp_bytes); + + var data_part = try std.ArrayList(u5).initCapacity(allocator, data_without_signature.len); + defer data_part.deinit(); + + data_part.appendSliceAssumeCapacity(data_without_signature); + + const overhang = (data_part.items.len * 5) % 8; + + if (overhang > 0) { + // add padding if data does not end at a byte boundary + try data_part.append(0); + + // if overhang is in (1..3) we need to add u5(0) padding two times + if (overhang < 3) { + try data_part.append(0); + } + } + + const data_part_u8 = try bech32.arrayListFromBase32(allocator, data_part.items); + defer data_part_u8.deinit(); + + try preimage.appendSlice(data_part_u8.items); + + return preimage; +} + +/// Represents a syntactically and semantically correct lightning BOLT11 invoice. +/// +/// There are three ways to construct a `Bolt11Invoice`: +/// 1. using [`InvoiceBuilder`] +/// 2. using [`Bolt11Invoice::from_signed`] +/// 3. using `str::parse::(&str)` (see [`Bolt11Invoice::from_str`]) +/// +/// [`Bolt11Invoice::from_str`]: crate::Bolt11Invoice#impl-FromStr +pub const Bolt11Invoice = struct { + signed_invoice: SignedRawBolt11Invoice, + + pub fn deinit(self: @This()) void { + self.signed_invoice.deinit(); + } + + pub fn fromStr(allocator: std.mem.Allocator, s: []const u8) !Bolt11Invoice { + const signed = try SignedRawBolt11Invoice.fromStr(allocator, s); + + return Bolt11Invoice.fromSigned(signed); + } + + pub fn fromSigned(signed_invoice: SignedRawBolt11Invoice) !@This() { + const invoice = Bolt11Invoice{ .signed_invoice = signed_invoice }; + + // invoice.check_field_counts()?; + // invoice.check_feature_bits()?; + // invoice.check_signature()?; + // invoice.check_amount()?; + + return invoice; + } + + /// Returns the amount if specified in the invoice as pico BTC. + fn amountPicoBtc(self: @This()) ?u64 { + return self.signed_invoice.raw_invoice.amountPicoBtc(); + } + + /// Check that amount is a whole number of millisatoshis + fn checkAmount(self: @This()) !void { + if (self.amountPicoBtc()) |amount_pico_btc| { + if (amount_pico_btc % 10 != 0) { + return error.ImpreciseAmount; + } + } + } + + /// Returns the amount if specified in the invoice as millisatoshis. + pub fn amountMilliSatoshis(self: @This()) ?u64 { + return if (self.signed_invoice.raw_invoice.amountPicoBtc()) |v| v / 10 else null; + } + + /// Returns the hash to which we will receive the preimage on completion of the payment + pub fn paymentHash(self: @This()) Sha256 { + return self.signed_invoice.raw_invoice.getKnownTag(.payment_hash) orelse @panic("expected payment_hash"); + } +}; + +/// Represents an syntactically correct [`Bolt11Invoice`] for a payment on the lightning network, +/// but without the signature information. +/// Decoding and encoding should not lead to information loss but may lead to different hashes. +/// +/// For methods without docs see the corresponding methods in [`Bolt11Invoice`]. +pub const RawBolt11Invoice = struct { + /// human readable part + hrp: RawHrp, + + /// data part + data: RawDataPart, + + pub fn deinit(self: RawBolt11Invoice) void { + self.data.deinit(); + } + + /// Hash the HRP as bytes and signatureless data part. + fn hashFromParts(allocator: std.mem.Allocator, hrp_bytes: []const u8, data_without_signature: []const u5) ![32]u8 { + const preimage = try constructInvoicePreimage(allocator, hrp_bytes, data_without_signature); + + defer preimage.deinit(); + + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + hasher.update(preimage.items); + + return hasher.finalResult(); + } + + pub fn getKnownTag(self: RawBolt11Invoice, comptime t: std.meta.Tag(TaggedField)) ?std.meta.TagPayload(TaggedField, t) { + for (self.data.tagged_fields.items) |f| { + return switch (f) { + .known => |kf| v: { + switch (kf) { + t => |ph| break :v ph, + else => break :v null, + } + }, + else => null, + } orelse continue; + } + + return null; + } + + /// Returns `null` if no amount is set or on overflow. + pub fn amountPicoBtc(self: RawBolt11Invoice) ?u64 { + if (self.hrp.raw_amount) |v| { + const multiplier: u64 = if (self.hrp.si_prefix) |si| si.multiplier() else 1_000_000_000_000; + + return std.math.mul(u64, v, multiplier) catch null; + } + + return null; + } +}; + +pub const Bolt11InvoiceSignature = struct { + value: core.secp256k1.RecoverableSignature, + + pub fn fromBase32(allocator: std.mem.Allocator, sig: []const u5) !Bolt11InvoiceSignature { + if (sig.len != 104) return errors.Bolt11ParseError.InvalidSliceLength; + + const recoverable_signature_bytes = try bech32.arrayListFromBase32(allocator, sig); + defer recoverable_signature_bytes.deinit(); + + const signature = recoverable_signature_bytes.items[0..64]; + const recovery_id = try core.secp256k1.RecoveryId.fromI32(recoverable_signature_bytes.items[64]); + + return .{ .value = try core.secp256k1.RecoverableSignature.fromCompact(signature, recovery_id) }; + } +}; + +/// Represents a signed [`RawBolt11Invoice`] with cached hash. The signature is not checked and may be +/// invalid. +/// +/// # Invariants +/// The hash has to be either from the deserialized invoice or from the serialized [`RawBolt11Invoice`]. +pub const SignedRawBolt11Invoice = struct { + /// The raw invoice that the signature belongs to + raw_invoice: RawBolt11Invoice, + + /// Hash of the [`RawBolt11Invoice`] that will be used to check the signature. + /// + /// * if the `SignedRawBolt11Invoice` was deserialized the hash is of from the original encoded form, + /// since it's not guaranteed that encoding it again will lead to the same result since integers + /// could have been encoded with leading zeroes etc. + /// * if the `SignedRawBolt11Invoice` was constructed manually the hash will be the calculated hash + /// from the [`RawBolt11Invoice`] + hash: [32]u8, + + /// signature of the payment request + signature: Bolt11InvoiceSignature, + + pub fn deinit(self: @This()) void { + self.raw_invoice.deinit(); + } + + pub fn fromStr(allocator: std.mem.Allocator, s: []const u8) !SignedRawBolt11Invoice { + const hrp, const data, const variant = try bech32.decode(allocator, s); + defer hrp.deinit(); + defer data.deinit(); + + if (variant == .bech32m) { + // Consider Bech32m addresses to be "Invalid Checksum", since that is what we'd get if + // we didn't support Bech32m (which lightning does not use). + return error.InvalidChecksum; + } + + if (data.items.len < 104) return error.TooShortDataPart; + + // rawhrp parse + const raw_hrp = try RawHrp.fromStr(hrp.items); + + var data_part = try RawDataPart.fromBase32(allocator, data.items[0 .. data.items.len - 104]); + errdefer data_part.deinit(); + + const hash_parts = try RawBolt11Invoice.hashFromParts(allocator, hrp.items, data.items[0 .. data.items.len - 104][0..]); + + return .{ + .signature = try Bolt11InvoiceSignature.fromBase32(allocator, data.items[data.items.len - 104 ..]), + .hash = hash_parts, + .raw_invoice = .{ + .hrp = raw_hrp, + .data = data_part, + }, + }; + } +}; + +pub const States = enum { + start, + parse_l, + parse_n, + parse_currency_prefix, + parse_amount_number, + parse_amount_si_prefix, + + fn nextState(self: @This(), read_byte: u8) errors.Bolt11ParseError!States { + // checking if symbol is not ascii + if (!std.ascii.isAscii(read_byte)) return errors.Bolt11ParseError.MalformedHRP; + + return switch (self) { + .start => if (read_byte == 'l') .parse_l else errors.Bolt11ParseError.MalformedHRP, + .parse_l => if (read_byte == 'n') .parse_n else errors.Bolt11ParseError.MalformedHRP, + .parse_n => if (!std.ascii.isDigit(read_byte)) .parse_currency_prefix else .parse_amount_number, + .parse_currency_prefix => if (!std.ascii.isDigit(read_byte)) .parse_currency_prefix else .parse_amount_number, + + .parse_amount_number => if (std.ascii.isDigit(read_byte)) + .parse_amount_number + else if (std.mem.lastIndexOfScalar(u8, "munp", read_byte) != null) + .parse_amount_si_prefix + else + errors.Bolt11ParseError.UnknownSiPrefix, + + .parse_amount_si_prefix => errors.Bolt11ParseError.UnknownSiPrefix, + }; + } + + fn isFinal(self: @This()) bool { + return !(self == .parse_l or self == .parse_n); + } +}; + +pub const StateMachine = struct { + state: States = .start, + position: usize = 0, + currency_prefix: ?struct { usize, usize } = null, + amount_number: ?struct { usize, usize } = null, + amount_si_prefix: ?struct { usize, usize } = null, + + fn updateRange(range: *?struct { usize, usize }, position: usize) void { + const new_range: struct { usize, usize } = if (range.*) |r| .{ r[0], r[1] + 1 } else .{ position, position + 1 }; + + range.* = new_range; + } + + fn step(self: *StateMachine, c: u8) errors.Bolt11ParseError!void { + const next_state = try self.state.nextState(c); + + switch (next_state) { + .parse_currency_prefix => StateMachine.updateRange(&self.currency_prefix, self.position), + .parse_amount_number => StateMachine.updateRange(&self.amount_number, self.position), + .parse_amount_si_prefix => StateMachine.updateRange(&self.amount_si_prefix, self.position), + else => {}, + } + + self.position += 1; + self.state = next_state; + } + + fn isFinal(self: *const StateMachine) bool { + return self.state.isFinal(); + } + + /// parseHrp - not allocating data, result is pointing on input! + pub fn parseHrp(input: []const u8) errors.Bolt11ParseError!struct { []const u8, []const u8, []const u8 } { + var sm = StateMachine{}; + for (input) |c| try sm.step(c); + + if (!sm.isFinal()) return errors.Bolt11ParseError.MalformedHRP; + + return .{ + if (sm.currency_prefix) |v| input[v[0]..v[1]] else "", + if (sm.amount_number) |v| input[v[0]..v[1]] else "", + if (sm.amount_si_prefix) |v| input[v[0]..v[1]] else "", + }; + } +}; + +/// Enum representing the crypto currencies (or networks) supported by this library +pub const Currency = enum { + /// Bitcoin mainnet + bitcoin, + + /// Bitcoin testnet + bitcoin_testnet, + + /// Bitcoin regtest + regtest, + + /// Bitcoin simnet + simnet, + + /// Bitcoin signet + signet, + + pub fn fromString(currency_prefix: []const u8) errors.Bolt11ParseError!Currency { + const convert = + std.StaticStringMap(Currency).initComptime(.{ + .{ "bc", Currency.bitcoin }, + .{ "tb", Currency.bitcoin_testnet }, + .{ "bcrt", Currency.regtest }, + .{ "sb", Currency.simnet }, + .{ "tbs", Currency.signet }, + }); + + return convert.get(currency_prefix) orelse errors.Bolt11ParseError.UnknownCurrency; + } +}; + +/// SI prefixes for the human readable part +pub const SiPrefix = enum { + /// 10^-3 + milli, + /// 10^-6 + micro, + /// 10^-9 + nano, + /// 10^-12 + pico, + + /// Returns the multiplier to go from a BTC value to picoBTC implied by this SiPrefix. + /// This is effectively 10^12 * the prefix multiplier + pub fn multiplier(self: SiPrefix) u64 { + return switch (self) { + .milli => 1_000_000_000, + .micro => 1_000_000, + .nano => 1_000, + .pico => 1, + }; + } + + /// Returns all enum variants of `SiPrefix` sorted in descending order of their associated + /// multiplier. + /// + /// This is not exported to bindings users as we don't yet support a slice of enums, and also because this function + /// isn't the most critical to expose. + pub fn valuesDesc() [4]SiPrefix { + return .{ .milli, .micro, .nano, .pico }; + } + + pub fn fromString(currency_prefix: []const u8) errors.Bolt11ParseError!SiPrefix { + if (currency_prefix.len == 0) return errors.Bolt11ParseError.UnknownSiPrefix; + return switch (currency_prefix[0]) { + 'm' => .milli, + 'u' => .micro, + 'n' => .nano, + 'p' => .pico, + else => errors.Bolt11ParseError.UnknownSiPrefix, + }; + } +}; + +/// Data of the [`RawBolt11Invoice`] that is encoded in the human readable part. +/// +/// This is not exported to bindings users as we don't yet support `Option` +pub const RawHrp = struct { + /// The currency deferred from the 3rd and 4th character of the bech32 transaction + currency: Currency, + + /// The amount that, multiplied by the SI prefix, has to be payed + raw_amount: ?u64, + + /// SI prefix that gets multiplied with the `raw_amount` + si_prefix: ?SiPrefix, + + pub fn fromStr(hrp: []const u8) !RawHrp { + const parts = try StateMachine.parseHrp(hrp); + + const currency = try Currency.fromString(parts[0]); + + const amount: ?u64 = if (parts[1].len > 0) try std.fmt.parseInt(u64, parts[1], 10) else null; + + const si_prefix: ?SiPrefix = if (parts[2].len == 0) null else v: { + const si = try SiPrefix.fromString(parts[2]); + + if (amount) |amt| { + _ = std.math.mul(u64, amt, si.multiplier()) catch return errors.Bolt11ParseError.IntegerOverflowError; + } + + break :v si; + }; + + return .{ + .currency = currency, + .raw_amount = amount, + .si_prefix = si_prefix, + }; + } +}; + +/// The number of bits used to represent timestamps as defined in BOLT 11. +const TIMESTAMP_BITS: usize = 35; + +/// The maximum timestamp as [`Duration::as_secs`] since the Unix epoch allowed by [`BOLT 11`]. +/// +/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md +pub const MAX_TIMESTAMP: u64 = (1 << TIMESTAMP_BITS) - 1; + +/// Default expiry time as defined by [BOLT 11]. +/// +/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md +pub const DEFAULT_EXPIRY_TIME: u64 = 3600; + +/// Default minimum final CLTV expiry as defined by [BOLT 11]. +/// +/// Note that this is *not* the same value as rust-lightning's minimum CLTV expiry, which is +/// provided in [`MIN_FINAL_CLTV_EXPIRY_DELTA`]. +/// +/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md +/// [`MIN_FINAL_CLTV_EXPIRY_DELTA`]: lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA +pub const DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA: u64 = 18; + +/// Data of the [`RawBolt11Invoice`] that is encoded in the data part +pub const RawDataPart = struct { + /// generation time of the invoice + timestamp: u64, + + /// tagged fields of the payment request + tagged_fields: std.ArrayList(RawTaggedField), + + pub fn deinit(self: RawDataPart) void { + for (self.tagged_fields.items) |f| f.deinit(); + + self.tagged_fields.deinit(); + } + + fn fromBase32(allocator: std.mem.Allocator, data: []const u5) !RawDataPart { + if (data.len < 7) return errors.Bolt11ParseError.TooShortDataPart; + + const timestamp: u64 = parseUintBe(u64, data[0..7][0..]) orelse @panic("7*5bit < 64bit, no overflow possible"); + + const tagged = try parseTaggedParts(allocator, data[7..]); + errdefer tagged.deinit(); + + return .{ + .timestamp = timestamp, + .tagged_fields = tagged, + }; + } +}; + +fn parseUintBe(comptime T: type, digits: []const u5) ?T { + var res: T = 0; + for (digits) |d| { + res = std.math.mul(T, res, 32) catch return null; + res = std.math.add(T, res, d) catch return null; + } + + return res; +} + +fn parseTaggedParts(allocator: std.mem.Allocator, _data: []const u5) !std.ArrayList(RawTaggedField) { + var parts = std.ArrayList(RawTaggedField).init(allocator); + errdefer parts.deinit(); + errdefer { + for (parts.items) |*p| { + p.deinit(); + } + } + + var data = _data; + + while (data.len > 0) { + if (data.len < 3) { + return errors.Bolt11ParseError.UnexpectedEndOfTaggedFields; + } + + // Ignore tag at data[0], it will be handled in the TaggedField parsers and + // parse the length to find the end of the tagged field's data + const len = parseUintBe(u16, data[1..3][0..]) orelse @panic("can't overflow"); + const last_element = 3 + len; + + if (data.len < last_element) { + return errors.Bolt11ParseError.UnexpectedEndOfTaggedFields; + } + + // Get the tagged field's data slice + const field = data[0..last_element][0..]; + + // Set data slice to remaining data + data = data[last_element..]; + + if (TaggedField.fromBase32(allocator, field)) |f| { + errdefer f.deinit(); + try parts.append(.{ .known = f }); + } else |err| switch (err) { + error.Skip => { + var un = try std.ArrayList(u5).initCapacity(allocator, field.len); + errdefer un.deinit(); + + un.appendSliceAssumeCapacity(field); + + try parts.append(.{ .unknown = un }); + continue; + }, + else => return err, + } + } + + return parts; +} + +/// Tagged field which may have an unknown tag +/// +/// This is not exported to bindings users as we don't currently support TaggedField +pub const RawTaggedField = union(enum) { + /// Parsed tagged field with known tag + known: TaggedField, + /// tagged field which was not parsed due to an unknown tag or undefined field semantics + unknown: std.ArrayList(u5), + + pub fn deinit(self: RawTaggedField) void { + switch (self) { + .unknown => |a| a.deinit(), + .known => |t| t.deinit(), + } + } +}; + +pub const Sha256 = [std.crypto.hash.sha2.Sha256.digest_length]u8; + +/// Tagged field with known tag +/// +/// For descriptions of the enum values please refer to the enclosed type's docs. +/// +/// This is not exported to bindings users as we don't yet support enum variants with the same name the struct contained +/// in the variant. +pub const TaggedField = union(enum) { + payment_hash: Sha256, + description: std.ArrayList(u8), + // payee_pub_key: core.secp256k1.PublicKey, + // description_hash: Sha256, + // expiry_time: u64, + + // min_final_cltv_expiry_delta: u64, + // fallback: Fallback, + + // PrivateRoute(PrivateRoute), + // PaymentSecret(PaymentSecret), + // PaymentMetadata(Vec), + // Features(Bolt11InvoiceFeatures), + + pub fn deinit(self: TaggedField) void { + switch (self) { + .description => |v| v.deinit(), + else => {}, + } + } + + fn fromBase32(allocator: std.mem.Allocator, field: []const u5) !TaggedField { + if (field.len < 3) return errors.Bolt11ParseError.UnexpectedEndOfTaggedFields; + + const tag = field[0]; + const field_data = field[3..]; + + return switch (@as(u8, tag)) { + constants.TAG_PAYMENT_HASH => v: { + if (field_data.len != 52) { + // "A reader MUST skip over […] a p, [or] h […] field that does not have data_length 52 […]." + + return errors.Bolt11ParseError.Skip; + } else { + const d = try bech32.arrayListFromBase32(allocator, field_data); + defer d.deinit(); + + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + + hasher.update(d.items); + + break :v TaggedField{ .payment_hash = hasher.finalResult() }; + } + }, + constants.TAG_DESCRIPTION => v: { + const bytes = try bech32.arrayListFromBase32(allocator, field_data); + errdefer bytes.deinit(); + + if (bytes.items.len > 639) return error.DescriptionDecodeError; + + break :v TaggedField{ .description = bytes }; + }, + else => return error.Skip, + }; + } +}; + +/// Fallback address in case no LN payment is possible +pub const Fallback = union(enum) { + program: []const u8, + // ripemd160 hash + pub_key_hash: [20]u8, + // ripemd160 hash + script_hash: [20]u8, +}; + +test "decode" { + const str = "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w"; + + const v = try SignedRawBolt11Invoice.fromStr(std.testing.allocator, str); + defer v.deinit(); + + try std.testing.expectEqual(v.raw_invoice.hrp, RawHrp{ .currency = .bitcoin, .raw_amount = null, .si_prefix = null }); + + // checking data + try std.testing.expectEqual(v.raw_invoice.data.timestamp, 1496314658); + + var hasher = std.crypto.hash.sha2.Sha256.init(.{}); + + var buf: [100]u8 = undefined; + + hasher.update(try std.fmt.hexToBytes(&buf, "0001020304050607080900010203040506070809000102030405060708090102")); + + try std.testing.expectEqual(hasher.finalResult(), v.raw_invoice.getKnownTag(.payment_hash)); + + try std.testing.expectEqualSlices(u8, "Please consider supporting this project", v.raw_invoice.getKnownTag(.description).?.items); + + // TODO: add other tags + + // end checking data + + try std.testing.expectEqual(.{ 0xc3, 0xd4, 0xe8, 0x3f, 0x64, 0x6f, 0xa7, 0x9a, 0x39, 0x3d, 0x75, 0x27, 0x7b, 0x1d, 0x85, 0x8d, 0xb1, 0xd1, 0xf7, 0xab, 0x71, 0x37, 0xdc, 0xb7, 0x83, 0x5d, 0xb2, 0xec, 0xd5, 0x18, 0xe1, 0xc9 }, v.hash); + + try std.testing.expectEqual(Bolt11InvoiceSignature{ + .value = try core.secp256k1.RecoverableSignature.fromCompact( + &.{ 0x38, 0xec, 0x68, 0x91, 0x34, 0x5e, 0x20, 0x41, 0x45, 0xbe, 0x8a, 0x3a, 0x99, 0xde, 0x38, 0xe9, 0x8a, 0x39, 0xd6, 0xa5, 0x69, 0x43, 0x4e, 0x18, 0x45, 0xc8, 0xaf, 0x72, 0x05, 0xaf, 0xcf, 0xcc, 0x7f, 0x42, 0x5f, 0xcd, 0x14, 0x63, 0xe9, 0x3c, 0x32, 0x88, 0x1e, 0xad, 0x0d, 0x6e, 0x35, 0x6d, 0x46, 0x7e, 0xc8, 0xc0, 0x25, 0x53, 0xf9, 0xaa, 0xb1, 0x5e, 0x57, 0x38, 0xb1, 0x1f, 0x12, 0x7f }, + try core.secp256k1.RecoveryId.fromI32(0), + ), + }, v.signature); +} diff --git a/src/mint/lightning/invoices/lib.zig b/src/mint/lightning/invoices/lib.zig new file mode 100644 index 0000000..3605bed --- /dev/null +++ b/src/mint/lightning/invoices/lib.zig @@ -0,0 +1,4 @@ +const std = @import("std"); + +pub usingnamespace @import("ripemd160.zig"); +pub usingnamespace @import("invoice.zig"); diff --git a/src/mint/lightning/invoices/ripemd160.zig b/src/mint/lightning/invoices/ripemd160.zig new file mode 100644 index 0000000..59be865 --- /dev/null +++ b/src/mint/lightning/invoices/ripemd160.zig @@ -0,0 +1,263 @@ +const std = @import("std"); +const testing = std.testing; +const mem = std.mem; + +pub const Ripemd160 = struct { + const Self = @This(); + pub const block_length = 64; + pub const digest_length = 20; + pub const Options = struct {}; + + s: [5]u32, + // Streaming Cache + buf: [64]u8 = undefined, + buf_len: u8 = 0, + total_len: u64 = 0, + + pub fn init(options: Options) Self { + _ = options; + return Self{ + .s = [_]u32{ + 0x67452301, + 0xEFCDAB89, + 0x98BADCFE, + 0x10325476, + 0xC3D2E1F0, + }, + }; + } + + pub fn update(d: *Self, b: []const u8) void { + var off: usize = 0; + + // Partial buffer exists from previous update. Copy into buffer then hash. + if (d.buf_len != 0 and d.buf_len + b.len >= 64) { + off += 64 - d.buf_len; + @memcpy(d.buf[d.buf_len..][0..off], b[0..off]); + + d.round(&d.buf); + d.buf_len = 0; + } + + // Full middle blocks. + while (off + 64 <= b.len) : (off += 64) { + d.round(b[off..][0..64]); + } + + // Copy any remainder for next pass. + const b_slice = b[off..]; + @memcpy(d.buf[d.buf_len..][0..b_slice.len], b_slice); + d.buf_len += @as(u8, @intCast(b[off..].len)); + + d.total_len += b.len; + } + + fn blockToWords(block: *const [block_length]u8) [16]u32 { + var words: [16]u32 = undefined; + for (words, 0..) |_, i| { + // zig fmt: off + words[i] = 0; + words[i] |= (@as(u32, block[i * 4 + 3]) << 24); + words[i] |= (@as(u32, block[i * 4 + 2]) << 16); + words[i] |= (@as(u32, block[i * 4 + 1]) << 8); + words[i] |= (@as(u32, block[i * 4 + 0]) << 0); + // zig fmt: on + } + return words; + } + + fn func(j: usize, x: u32, y: u32, z: u32) u32 { + return switch (j) { + // f(j, x, y, z) = x XOR y XOR z (0 <= j <= 15) + 0...15 => x ^ y ^ z, + // f(j, x, y, z) = (x AND y) OR (NOT(x) AND z) (16 <= j <= 31) + 16...31 => (x & y) | (~x & z), + // f(j, x, y, z) = (x OR NOT(y)) XOR z (32 <= j <= 47) + 32...47 => (x | ~y) ^ z, + // f(j, x, y, z) = (x AND z) OR (y AND NOT(z)) (48 <= j <= 63) + 48...63 => (x & z) | (y & ~z), + // f(j, x, y, z) = x XOR (y OR NOT(z)) (64 <= j <= 79) + // !!! omg xor and or 64 + 64...79 => x ^ (y | ~z), + else => unreachable, + }; + } + + fn round(d: *Self, b: *const [block_length]u8) void { + var leftA = d.s[0]; + var leftB = d.s[1]; + var leftC = d.s[2]; + var leftD = d.s[3]; + var leftE = d.s[4]; + + var rightA = d.s[0]; + var rightB = d.s[1]; + var rightC = d.s[2]; + var rightD = d.s[3]; + var rightE = d.s[4]; + + const words: [16]u32 = blockToWords(b); + var tmp: u32 = undefined; + var j: usize = 0; + while (j < 80) : (j += 1) { + // zig fmt: off + tmp = std.math.rotl(u32, leftA + +% func(j, leftB, leftC, leftD) + +% words[left_selecting_words[j]] + +% left_K[j / 16], + left_tmp_shift_amount[j]) +% leftE; + // zig fmt: on + leftA = leftE; + leftE = leftD; + leftD = std.math.rotl(u32, leftC, 10); + leftC = leftB; + leftB = tmp; + + // zig fmt: off + tmp = std.math.rotl(u32, rightA + +% func(79 - j, rightB, rightC, rightD) + +% words[right_selecting_words[j]] + +% right_K[j / 16], + right_tmp_shift_amount[j]) +% rightE; + // zig fmt: on + rightA = rightE; + rightE = rightD; + rightD = std.math.rotl(u32, rightC, 10); + rightC = rightB; + rightB = tmp; + } + + tmp = d.s[1] +% leftC +% rightD; + d.s[1] = d.s[2] +% leftD +% rightE; + d.s[2] = d.s[3] +% leftE +% rightA; + d.s[3] = d.s[4] +% leftA +% rightB; + d.s[4] = d.s[0] +% leftB +% rightC; + d.s[0] = tmp; + } + + pub fn final(d: *Self, out: *[digest_length]u8) void { + // The buffer here will never be completely full. + @memset(d.buf[d.buf_len..], 0); + + // Append padding bits. + d.buf[d.buf_len] = 0x80; + d.buf_len += 1; + + // > 448 mod 512 so need to add an extra round to wrap around. + if (64 - d.buf_len < 8) { + d.round(d.buf[0..]); + @memset(d.buf[0..], 0); + } + + // Append message length in more simple way + const len = (d.total_len * 8); + mem.writeInt(u64, d.buf[56..64], len, .little); + + d.round(d.buf[0..]); + + for (d.s, 0..) |s, j| { + mem.writeInt(u32, out[4 * j ..][0..4], s, .little); + } + } + + pub fn hash(b: []const u8, out: *[digest_length]u8, options: Options) void { + var d = Ripemd160.init(options); + d.update(b); + d.final(out); + } +}; + +test "test vectors" { + const input = [_][]const u8{ + "", + "a", + "abc", + "message digest", + "abcdefghijklmnopqrstuvwxyz", + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "1234567890" ** 8, + "a" ** 1000000, + }; + const output = [_][]const u8{ + "9c1185a5c5e9fc54612808977ee8f548b2258d31", + "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe", + "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc", + "5d0689ef49d2fae572b881b123a85ffa21595f36", + "f71c27109c692c1b56bbdceb5b9d2865b3708dbc", + "12a053384a9c0c88e405a06c27dcf49ada62eb2b", + "b0e20b6e3116640286ed3a87a5713079b21f5189", + "9b752e45573d4b39f4dbd3323cab82bf63326bfb", + "52783243c1697bdbe16d37f97f68f08325dc1528", + }; + for (0..input.len) |i| { + var expected_output: [Ripemd160.digest_length]u8 = undefined; + _ = try std.fmt.hexToBytes(&expected_output, output[i]); + var actual_output: [Ripemd160.digest_length]u8 = undefined; + Ripemd160.hash(input[i], &actual_output, .{}); + try testing.expectEqualSlices(u8, &expected_output, &actual_output); + } +} + +test "streaming" { + var h = Ripemd160.init(.{}); + var out: [Ripemd160.digest_length]u8 = undefined; + h.final(&out); + try testing.expectEqualSlices(u8, &[_]u8{ + 0x9c, 0x11, 0x85, 0xa5, 0xc5, 0xe9, 0xfc, 0x54, 0x61, 0x28, + 0x08, 0x97, 0x7e, 0xe8, 0xf5, 0x48, 0xb2, 0x25, 0x8d, 0x31, + }, &out); + + h = Ripemd160.init(.{}); + h.update("abc"); + h.final(&out); + try testing.expectEqualSlices(u8, &[_]u8{ + 0x8e, 0xb2, 0x08, 0xf7, 0xe0, 0x5d, 0x98, 0x7a, 0x9b, 0x04, + 0x4a, 0x8e, 0x98, 0xc6, 0xb0, 0x87, 0xf1, 0x5a, 0x0b, 0xfc, + }, &out); + + h = Ripemd160.init(.{}); + h.update("a"); + h.update("b"); + h.update("c"); + h.final(&out); + try testing.expectEqualSlices(u8, &[_]u8{ + 0x8e, 0xb2, 0x08, 0xf7, 0xe0, 0x5d, 0x98, 0x7a, 0x9b, 0x04, + 0x4a, 0x8e, 0x98, 0xc6, 0xb0, 0x87, 0xf1, 0x5a, 0x0b, 0xfc, + }, &out); +} + +const left_selecting_words = [80]u32{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13, +}; + +const right_selecting_words = [80]u32{ + 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11, +}; + +const left_tmp_shift_amount = [80]u32{ + 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6, +}; + +const right_tmp_shift_amount = [80]u32{ + 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11, +}; + +const left_K = [5]u32{ 0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E }; +const right_K = [5]u32{ 0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000 }; diff --git a/src/mint/lightning/lib.zig b/src/mint/lightning/lib.zig new file mode 100644 index 0000000..72b4513 --- /dev/null +++ b/src/mint/lightning/lib.zig @@ -0,0 +1,4 @@ +pub const lnbits = @import("lnbits.zig"); +pub const invoice = @import("invoices/lib.zig"); + +pub const Lightning = @import("lightning.zig"); diff --git a/src/mint/lightning/lightning.zig b/src/mint/lightning/lightning.zig new file mode 100644 index 0000000..81d2ef6 --- /dev/null +++ b/src/mint/lightning/lightning.zig @@ -0,0 +1,61 @@ +const std = @import("std"); +const invoice = @import("invoices/lib.zig"); +const model = @import("../model.zig"); +const Self = @This(); + +// These two fields are the same as before +ptr: *anyopaque, + +createInvoiceFn: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator, amount: u64) anyerror!model.CreateInvoiceResult, + +payInvoiceFn: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator, payment_request: []const u8) anyerror!model.PayInvoiceResult, + +isInvoicePaidFn: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator, invoice: []const u8) anyerror!bool, + +// This is new +pub fn init(ptr: anytype) Self { + const T = @TypeOf(ptr); + const ptr_info = @typeInfo(T); + + const gen = struct { + pub fn createInvoice(pointer: *anyopaque, allocator: std.mem.Allocator, amount: u64) !model.CreateInvoiceResult { + const self: T = @ptrCast(@alignCast(pointer)); + return ptr_info.Pointer.child.createInvoice(self, allocator, amount); + } + pub fn payInvoice(pointer: *anyopaque, allocator: std.mem.Allocator, payment_request: []const u8) !model.PayInvoiceResult { + const self: T = @ptrCast(@alignCast(pointer)); + return ptr_info.Pointer.child.payInvoice(self, allocator, payment_request); + } + + pub fn isInvoicePaid(pointer: *anyopaque, allocator: std.mem.Allocator, inv: []const u8) !bool { + const self: T = @ptrCast(@alignCast(pointer)); + return ptr_info.Pointer.child.isInvoicePaid(self, allocator, inv); + } + }; + + return .{ + .ptr = ptr, + + .createInvoiceFn = gen.createInvoice, + .isInvoicePaidFn = gen.isInvoicePaid, + .payInvoiceFn = gen.payInvoice, + }; +} + +pub fn isInvoicePaid(self: Self, allocator: std.mem.Allocator, inv: []const u8) anyerror!bool { + return self.isInvoicePaidFn(self.ptr, allocator, inv); +} + +pub fn payInvoice(self: Self, allocator: std.mem.Allocator, payment_request: []const u8) !model.PayInvoiceResult { + return self.payInvoiceFn(self.ptr, allocator, payment_request); +} + +/// Caller is own [model.CreateInvoiceResult], so he responsible to call deinit on it +pub fn createInvoice(self: Self, allocator: std.mem.Allocator, amount: u64) anyerror!model.CreateInvoiceResult { + return self.createInvoiceFn(self.ptr, allocator, amount); +} + +/// Decoding invoice from payment request to [Bolt11Invoice], caller is responsible to call deinit on succ result over [Bolt11Invoice] +pub fn decodeInvoice(_: Self, allocator: std.mem.Allocator, payment_request: []const u8) !invoice.Bolt11Invoice { + return invoice.Bolt11Invoice.fromStr(allocator, payment_request); +} diff --git a/src/mint/lightning/lnbits.zig b/src/mint/lightning/lnbits.zig new file mode 100644 index 0000000..d5a254e --- /dev/null +++ b/src/mint/lightning/lnbits.zig @@ -0,0 +1,301 @@ +const std = @import("std"); +const model = @import("../model.zig"); +const Lightning = @import("lightning.zig"); + +pub const HttpError = std.http.Client.RequestError || std.http.Client.Request.FinishError || std.http.Client.Request.WaitError || error{ ReadBodyError, WrongJson }; + +pub const LightningError = HttpError || std.Uri.ParseError || std.mem.Allocator.Error || error{ + NotFound, + Unauthorized, + PaymentFailed, +}; + +pub const Settings = struct { + admin_key: ?[]const u8, + url: ?[]const u8, +}; + +pub const LnBitsLightning = struct { + client: LNBitsClient, + + pub fn init(allocator: std.mem.Allocator, admin_key: []const u8, lnbits_url: []const u8) !@This() { + return .{ + .client = try LNBitsClient.init(allocator, admin_key, lnbits_url), + }; + } + + pub fn deinit(self: *@This()) void { + self.client.deinit(); + } + + pub fn lightning(self: *@This()) Lightning { + return Lightning.init(self); + } + + pub fn isInvoicePaid(self: *@This(), allocator: std.mem.Allocator, invoice: []const u8) !bool { + const decoded_invoice = try self.lightning().decodeInvoice(allocator, invoice); + defer decoded_invoice.deinit(); + + return self.client.isInvoicePaid(allocator, &decoded_invoice.paymentHash()); + } + + pub fn createInvoice(self: *@This(), allocator: std.mem.Allocator, amount: u64) !model.CreateInvoiceResult { + return try self.client.createInvoice(allocator, .{ + .amount = amount, + .unit = "sat", + .memo = null, + .expiry = 10000, + .webhook = null, + .internal = null, + }); + } + + pub fn payInvoice(self: *@This(), allocator: std.mem.Allocator, payment_request: []const u8) !model.PayInvoiceResult { + return try self.client.payInvoice(allocator, payment_request); + } +}; + +pub const LNBitsClient = struct { + admin_key: []const u8, + lnbits_url: std.Uri, + client: std.http.Client, + + pub fn init( + allocator: std.mem.Allocator, + admin_key: []const u8, + lnbits_url: []const u8, + ) !LNBitsClient { + const url = try std.Uri.parse(lnbits_url); + + var client = std.http.Client{ + .allocator = allocator, + }; + errdefer client.deinit(); + + return .{ + .admin_key = admin_key, + .lnbits_url = url, + .client = client, + }; + } + + pub fn deinit(self: *@This()) void { + self.client.deinit(); + } + + // get - request get, caller is owner of result slice (should deallocate it with allocator passed as argument) + pub fn get( + self: *@This(), + allocator: std.mem.Allocator, + endpoint: []const u8, + ) LightningError![]const u8 { + var buf: [100]u8 = undefined; + var b: []u8 = buf[0..]; + + const uri = self.lnbits_url.resolve_inplace(endpoint, &b) catch return std.Uri.ParseError.UnexpectedCharacter; + + const header_buf = try allocator.alloc(u8, 1024 * 1024 * 4); + defer allocator.free(header_buf); + + var req = try self.client.open(.GET, uri, .{ + .server_header_buffer = header_buf, + .extra_headers = &.{ + .{ + .name = "X-Api-Key", + .value = self.admin_key, + }, + }, + }); + defer req.deinit(); + + try req.send(); + try req.finish(); + try req.wait(); + + if (req.response.status != .ok) { + if (req.response.status == .not_found) return LightningError.NotFound; + } + + var rdr = req.reader(); + const body = rdr.readAllAlloc(allocator, 1024 * 1024 * 4) catch |err| { + std.log.debug("read body error: {any}", .{err}); + return error.ReadBodyError; + }; + errdefer allocator.free(body); + + return body; + } + + pub fn post( + self: *@This(), + allocator: std.mem.Allocator, + endpoint: []const u8, + req_body: []const u8, + ) LightningError![]const u8 { + var buf: [100]u8 = undefined; + var b: []u8 = buf[0..]; + + const uri = self.lnbits_url.resolve_inplace(endpoint, &b) catch return std.Uri.ParseError.UnexpectedCharacter; + + const header_buf = try allocator.alloc(u8, 1024 * 1024 * 4); + defer allocator.free(header_buf); + + var req = try self.client.open(.POST, uri, .{ + .server_header_buffer = header_buf, + .extra_headers = &.{ + .{ + .name = "X-Api-Key", + .value = self.admin_key, + }, + + .{ + .name = "accept", + .value = "*/*", + }, + }, + }); + defer req.deinit(); + + req.transfer_encoding = .{ .content_length = req_body.len }; + + try req.send(); + try req.writeAll(req_body); + try req.finish(); + try req.wait(); + + if (req.response.status != .ok) { + if (req.response.status == .not_found) return LightningError.NotFound; + if (req.response.status == .unauthorized) return LightningError.Unauthorized; + } + + var rdr = req.reader(); + const body = rdr.readAllAlloc(allocator, 1024 * 1024 * 4) catch |err| { + std.log.debug("read post body error: {any}", .{err}); + return error.ReadBodyError; + }; + errdefer allocator.free(body); + + return body; + } + + /// createInvoice - creating invoice + /// note: after success call u need to call deinit on result using alloactor that u pass as argument to this func. + pub fn createInvoice(self: *@This(), allocator: std.mem.Allocator, params: model.CreateInvoiceParams) !model.CreateInvoiceResult { + const req_body = try std.json.stringifyAlloc(allocator, ¶ms, .{}); + + const res = try self.post(allocator, "api/v1/payments", req_body); + + const parsed = std.json.parseFromSlice(std.json.Value, allocator, res, .{ .allocate = .alloc_always }) catch return error.WrongJson; + + const payment_request = parsed.value.object.get("payment_request") orelse unreachable; + const payment_hash = parsed.value.object.get("payment_hash") orelse unreachable; + + const pr = switch (payment_request) { + .string => |v| val: { + const result = try allocator.alloc(u8, v.len); + @memcpy(result, v); + break :val result; + }, + else => { + unreachable; + }, + }; + errdefer allocator.free(pr); + + const ph = switch (payment_hash) { + .string => |v| val: { + const result = try allocator.alloc(u8, v.len); + @memcpy(result, v); + break :val result; + }, + else => { + unreachable; + }, + }; + errdefer allocator.free(ph); + + return .{ + .payment_hash = ph, + .payment_request = pr, + }; + } + + /// payInvoice - paying invoice + /// note: after success call u need to call deinit on result using alloactor that u pass as argument to this func. + pub fn payInvoice(self: *@This(), allocator: std.mem.Allocator, bolt11: []const u8) !model.PayInvoiceResult { + const req_body = try std.json.stringifyAlloc(allocator, &.{ .out = true, .bolt11 = bolt11 }, .{}); + + const res = try self.post(allocator, "api/v1/payments", req_body); + + const parsed = std.json.parseFromSlice(std.json.Value, allocator, res, .{ .allocate = .alloc_always }) catch return error.WrongJson; + + const payment_hash = parsed.value.object.get("payment_hash") orelse unreachable; + + const ph = switch (payment_hash) { + .string => |v| val: { + const result = try allocator.alloc(u8, v.len); + @memcpy(result, v); + break :val result; + }, + else => { + unreachable; + }, + }; + errdefer allocator.free(ph); + + return .{ + .payment_hash = ph, + .total_fees = 0, + }; + } + + /// isInvoicePaid - paying invoice + /// note: after success call u need to call deinit on result using alloactor that u pass as argument to this func. + pub fn isInvoicePaid(self: *@This(), allocator: std.mem.Allocator, payment_hash: []const u8) !bool { + const endpoint = try std.fmt.allocPrint( + allocator, + "api/v1/payments/{s}", + .{payment_hash}, + ); + defer allocator.free(endpoint); + + const res = try self.get(allocator, endpoint); + const parsed = std.json.parseFromSlice(std.json.Value, allocator, res, .{ .allocate = .alloc_always }) catch return error.WrongJson; + + const is_paid = parsed.value.object.get("paid") orelse unreachable; + + return switch (is_paid) { + .bool => |v| v, + else => false, + }; + } +}; + +test "test_decode_invoice" { + var client = try LnBitsLightning.init(std.testing.allocator, "admin_key", "http://localhost:5000"); + defer client.deinit(); + + const lightning = client.lightning(); + + const invoice = "lnbcrt55550n1pjga687pp5ac8ja6n5hn90huztxxp746w48vtj8ys5uvze6749dvcsd5j5sdvsdqqcqzzsxqyz5vqsp5kzzq0ycxspxjygsxkfkexkkejjr5ggeyl56mwa7s0ygk2q8z92ns9qyyssqt7myq7sryffasx8v47al053ut4vqts32e9hvedvs7eml5h9vdrtj3k5m72yex5jv355jpuzk2xjjn5468cz87nhp50jyr2al2a5zjvgq2xs5uq"; + + const decoded_invoice = try lightning.decodeInvoice(std.testing.allocator, invoice); + defer decoded_invoice.deinit(); + + try std.testing.expectEqual(5_555 * 1_000, decoded_invoice.amountMilliSatoshis()); +} + +test "test_decode_invoice_invalid" { + var client = try LnBitsLightning.init(std.testing.allocator, "admin_key", "http://localhost:5000"); + defer client.deinit(); + + const lightning = client.lightning(); + + const invoice = "lnbcrt55550n1pjga689pp5ac8ja6n5hn90huztyxp746w48vtj8ys5uvze6749dvcsd5j5sdvsdqqcqzzsxqyz5vqsp5kzzq0ycxspxjygsxkfkexkkejjr5ggeyl56mwa7s0ygk2q8z92ns9qyyssqt7myq7sryffasx8v47al053ut4vqts32e9hvedvs7eml5h9vdrtj3k5m72yex5jv355jpuzk2xjjn5468cz87nhp50jyr2al2a5zjvgq2xs5uw"; + + // expecting a error + try std.testing.expect(if (lightning.decodeInvoice(std.testing.allocator, invoice)) |d| v: { + d.deinit(); + break :v false; + } else |_| true); +} diff --git a/src/mint/mint.zig b/src/mint/mint.zig index 64a8cad..a0c714a 100644 --- a/src/mint/mint.zig +++ b/src/mint/mint.zig @@ -1,121 +1,19 @@ const std = @import("std"); - const core = @import("../core/lib.zig"); -const database = @import("database/database.zig"); - -const MintConfig = @import("config.zig").MintConfig; +const MintInfo = core.nuts.MintInfo; +const secp256k1 = core.secp256k1; +const bip32 = core.bip32; +/// Cashu Mint pub const Mint = struct { - const Self = @This(); - - keyset: core.keyset.MintKeyset, - config: MintConfig, - db: database.Database, - dhke: core.Dhke, - allocator: std.mem.Allocator, - - // init - initialized Mint using config - pub fn init(allocator: std.mem.Allocator, config: MintConfig) !Mint { - var keyset = try core.keyset.MintKeyset.init( - allocator, - config.privatekey, - config.derivation_path orelse &.{}, - ); - errdefer keyset.deinit(); - - const db = try database.InMemory.init(allocator); - errdefer db.deinit(); - - return .{ - .keyset = keyset, - .config = config, - .db = db, - .allocator = allocator, - .dhke = try core.Dhke.init(allocator), - }; - } - - pub fn deinit(self: *@This()) void { - self.keyset.deinit(); - self.db.deinit(); - self.dhke.deinit(); - } - - pub fn checkedUsedProofs(self: *const Self, tx: database.Tx, proofs: []const core.proof.Proof) !void { - const used_proofs = try self.db.getUsedProofs(tx, self.allocator); - defer self.allocator.free(used_proofs); - - for (used_proofs) |used_proof| { - for (proofs) |proof| if (std.meta.eql(used_proof, proof)) return error.ProofAlreadyUsed; - } - } - - fn hasDuplicatePubkeys(allocator: std.mem.Allocator, outputs: []const core.BlindedMessage) !bool { - var uniq = std.AutoHashMap([64]u8, void).init(allocator); - defer uniq.deinit(); - - for (outputs) |x| { - const res = try uniq.getOrPut(x.b_.pk.data); - if (res.found_existing) return true; - } - - return false; - } - - pub fn createBlindedSignatures( - self: *const Self, - allocator: std.mem.Allocator, - blinded_messages: []const core.BlindedMessage, - keyset: core.keyset.MintKeyset, - ) ![]core.BlindedSignature { - var res = try std.ArrayList(core.BlindedSignature).initCapacity(allocator, blinded_messages.len); - errdefer res.deinit(); - - for (blinded_messages) |blinded_msg| { - const priv_key = keyset.private_keys.get(blinded_msg.amount) orelse return error.PrivateKeyNotFound; - - const blinded_sig = try self.dhke.step2Bob(blinded_msg.b_, priv_key); - - res.appendAssumeCapacity(.{ - .amount = blinded_msg.amount, - .c_ = blinded_sig, - .id = keyset.keyset_id, - }); - } - return try res.toOwnedSlice(); - } - - pub fn swap( - self: *const Self, - allocator: std.mem.Allocator, - proofs: []const core.proof.Proof, - blinded_messages: []const core.BlindedMessage, - keyset: core.keyset.MintKeyset, - ) ![]const core.BlindedSignature { - var tx = try self.db.beginTx(allocator); - errdefer tx.rollback() catch |err| std.log.debug("rollback err {any}", .{err}); - - try self.checkedUsedProofs(tx, proofs); - - if (try Self.hasDuplicatePubkeys(self.allocator, blinded_messages)) return error.SwapHasDuplicatePromises; - - const sum_proofs = core.proof.Proof.totalAmount(proofs); - - const promises = try self.createBlindedSignatures(allocator, blinded_messages, keyset); - errdefer allocator.free(promises); - - const amount_promises = core.BlindedSignature.totalAmount(promises); - - if (sum_proofs != amount_promises) { - std.log.debug("Swap amount mismatch: {d} != {d}", .{ - sum_proofs, amount_promises, - }); - return error.SwapAmountMismatch; - } - - try self.db.addUsedProofs(tx, proofs); - try tx.commit(); - - return promises; - } + /// Mint Url + mint_url: std.Uri, + /// Mint Info + mint_info: MintInfo, + /// Mint Storage backend + // pub localstore: Arc + Send + Sync>, + /// Active Mint Keysets + // keysets: Arc>>, + secp_ctx: secp256k1.Secp256k1, + xpriv: bip32.ExtendedPrivKey, }; diff --git a/src/mint/routes/default.zig b/src/mint/routes/default.zig index 1f120e3..4d937f8 100644 --- a/src/mint/routes/default.zig +++ b/src/mint/routes/default.zig @@ -2,6 +2,7 @@ const std = @import("std"); const httpz = @import("httpz"); const mint_lib = @import("../mint.zig"); const core = @import("../../core/lib.zig"); +const zul = @import("zul"); pub fn getKeys(mint: *const mint_lib.Mint, req: *httpz.Request, res: *httpz.Response) !void { _ = req; // autofix @@ -65,3 +66,152 @@ pub fn swap(mint: *const mint_lib.Mint, req: *httpz.Request, res: *httpz.Respons try res.json(.{ .signature = response }, .{}); } + +pub fn mintQuoteBolt11(mint: *const mint_lib.Mint, req: *httpz.Request, res: *httpz.Response) !void { + // TODO figure out what error in parsing + // status code 200 is implicit. + + // The json helper will automatically set the res.content_type = httpz.ContentType.JSON; + // Here we're passing an inferred anonymous structure, but you can pass anytype + // (so long as it can be serialized using std.json.stringify) + + const key = zul.UUID.v4(); + + // dont need to call deinit because res.allocator is arena + const data = try std.json.parseFromSlice(core.primitives.PostMintQuoteBolt11Request, res.arena, req.body().?, .{}); + + // not need to deallocate due arena res + const inv = try mint.createInvoice(res.arena, &key.bin, data.value.amount); + + const quote = core.primitives.Bolt11MintQuote{ + .quote_id = key, + .payment_request = inv.payment_request, + // plus 30 minutes + .expiry = @as(u64, @intCast(std.time.timestamp())) + 30 * 60, + .paid = false, + }; + + var tx = try mint.db.beginTx(res.arena); + try mint.db.addBolt11MintQuote(res.arena, tx, quote); + try tx.commit(); + + try res.json("e, .{}); +} + +pub fn mintBolt11(mint: *const mint_lib.Mint, req: *httpz.Request, res: *httpz.Response) !void { + // The json helper will automatically set the res.content_type = httpz.ContentType.JSON; + // Here we're passing an inferred anonymous structure, but you can pass anytype + // (so long as it can be serialized using std.json.stringify) + + // dont need to call deinit because res.allocator is arena + const data = try std.json.parseFromSlice(core.primitives.PostMintBolt11Request, res.arena, req.body().?, .{}); + + var tx = try mint.db.beginTx(res.arena); + + // we dont need to deallocate due arena allocator + const signatures = try mint.mintBolt11Tokens(res.arena, tx, data.value.quote, data.value.outputs.value.items, mint.keyset); + + // no need deallocate + // check zul.uuid parse quote id + var old_quote = try mint.db.getBolt11MintQuote(res.arena, tx, try zul.UUID.parse(data.value.quote)); + + old_quote.paid = true; + + try mint.db.updateBolt11MintQuote(res.arena, tx, old_quote); + + try tx.commit(); + + try res.json(core.primitives.PostMintBolt11Response{ + .signatures = signatures, + }, .{}); +} + +pub fn getMintQuoteBolt11(mint: *const mint_lib.Mint, req: *httpz.Request, res: *httpz.Response) !void { + const quote_id = req.param("quote_id").?; + + std.log.debug("get_quote: {any}", .{quote_id}); + + var tx = try mint.db.beginTx(res.arena); + + var quote = try mint.db.getBolt11MintQuote(res.arena, tx, try zul.UUID.parse(quote_id)); + + try tx.commit(); + + quote.paid = try mint.lightning.isInvoicePaid(res.arena, quote.payment_request); + + try res.json("e, .{}); +} + +pub fn meltQuoteBolt11(mint: *const mint_lib.Mint, req: *httpz.Request, res: *httpz.Response) !void { + + // dont need to call deinit because res.allocator is arena + const request = try std.json.parseFromSlice(core.primitives.PostMeltQuoteBolt11Request, res.arena, req.body().?, .{}); + + const invoice = try mint.lightning.decodeInvoice(res.arena, request.value.request); + + const amount = invoice.amountMilliSatoshis() orelse return error.InvalidInvoiceAmount; + + const fee_reserve = try mint.feeReserve(amount) / 1000; + + std.log.debug("fee reserve : {any}", .{fee_reserve}); + + const amount_sat = amount / 1000; + + const key = zul.UUID.v4(); + const quote = core.primitives.Bolt11MeltQuote{ + .quote_id = key, + .amount = amount_sat, + .fee_reserve = fee_reserve, + .expiry = @as(u64, @intCast(std.time.timestamp())) + 30 * 60, + .payment_request = request.value.request, + .paid = false, + }; + + var tx = try mint.db.beginTx(res.arena); + + try mint.db.addBolt11MeltQuote(res.arena, tx, quote); + + try tx.commit(); + + try res.json("e, .{}); +} + +pub fn getMeltQuoteBolt11(mint: *const mint_lib.Mint, req: *httpz.Request, res: *httpz.Response) !void { + const quote_id = req.param("quote_id").?; + + std.log.debug("get_quote: {any}", .{quote_id}); + + var tx = try mint.db.beginTx(res.arena); + + var quote = try mint.db.getBolt11MeltQuote(res.arena, tx, try zul.UUID.parse(quote_id)); + + try tx.commit(); + + quote.paid = try mint.lightning.isInvoicePaid(res.arena, quote.payment_request); + + try res.json("e, .{}); +} + +pub fn meltBolt11(mint: *const mint_lib.Mint, req: *httpz.Request, res: *httpz.Response) !void { + // dont need to call deinit because res.allocator is arena + const data = try std.json.parseFromSlice(core.primitives.PostMeltBolt11Request, res.arena, req.body().?, .{}); + + var tx = try mint.db.beginTx(res.arena); + + // we dont need to deallocate due arena allocator + const signatures = try mint.mintBolt11Tokens(res.arena, tx, data.value.quote, data.value.outputs.value.items, mint.keyset); + + // no need deallocate + // check zul.uuid parse quote id + var old_quote = try mint.db.getBolt11MintQuote(res.arena, tx, try zul.UUID.parse(data.value.quote)); + + old_quote.paid = true; + + try mint.db.updateBolt11MintQuote(res.arena, tx, old_quote); + + try tx.commit(); + + try res.json(core.primitives.PostMintBolt11Response{ + .signatures = signatures, + }, .{}); +} diff --git a/src/mint/server.zig b/src/mint/server.zig deleted file mode 100644 index cbace8e..0000000 --- a/src/mint/server.zig +++ /dev/null @@ -1,41 +0,0 @@ -const std = @import("std"); -const mint_lib = @import("mint.zig"); -const httpz = @import("httpz"); -const routes = @import("routes/lib.zig"); - -pub fn runServer( - allocator: std.mem.Allocator, - mint: *const mint_lib.Mint, -) !void { - std.log.debug("start running server {any}", .{ - mint, - }); - - var server = try httpz.ServerApp(*const mint_lib.Mint).init(allocator, .{ - .port = mint.config.server.port, - .address = mint.config.server.host, - }, mint); - - // overwrite the default notFound handler - // server.notFound(notFound); - - // overwrite the default error handler - server.errorHandler(errorHandler); - - var router = server.router(); - - router.get("/v1/keys", routes.default.getKeys); - router.get("/v1/keys/:id", routes.default.getKeysById); - router.get("/v1/keysets", routes.default.getKeysets); - router.post("/v1/swap", routes.default.swap); - - return server.listen(); -} - -// note that the error handler return `void` and not `!void` -fn errorHandler(_: *const mint_lib.Mint, req: *httpz.Request, res: *httpz.Response, err: anyerror) void { - res.status = 500; - res.body = @errorName(err); - - std.log.warn("httpz: unhandled exception for request: {s}\nErr: {}", .{ req.url.raw, err }); -} diff --git a/src/mint/types.zig b/src/mint/types.zig new file mode 100644 index 0000000..48f4e59 --- /dev/null +++ b/src/mint/types.zig @@ -0,0 +1,96 @@ +const nuts = @import("../core/lib.zig").nuts; +const std = @import("std"); +const amount_lib = @import("../core/lib.zig").amount; +const CurrencyUnit = @import("../core/lib.zig").nuts.CurrencyUnit; +const MintQuoteState = @import("../core/lib.zig").nuts.nut04.QuoteState; +const MeltQuoteState = @import("../core/lib.zig").nuts.nut05.QuoteState; +const zul = @import("zul"); + +/// Mint Quote Info +pub const MintQuote = struct { + /// Quote id + id: [16]u8, + /// Mint Url + mint_url: std.Uri, + /// Amount of quote + amount: amount_lib.Amount, + /// Unit of quote + unit: CurrencyUnit, + /// Quote payment request e.g. bolt11 + request: []const u8, + /// Quote state + state: MintQuoteState, + /// Expiration time of quote + expiry: u64, + /// Value used by ln backend to look up state of request + request_lookup_id: []const u8, + + /// Create new [`MintQuote`] + pub fn init( + mint_url: std.Uri, + request: []const u8, + unit: CurrencyUnit, + amount: amount_lib.Amount, + expiry: u64, + request_lookup_id: []const u8, + ) MintQuote { + const id = zul.UUID.v4(); + + return .{ + .mint_url = mint_url, + .id = id.bin, + .amount = amount, + .unit = unit, + .request = request, + .state = .unpaid, + .expiry = expiry, + .request_lookup_id = request_lookup_id, + }; + } +}; + +/// Melt Quote Info +pub const MeltQuote = struct { + /// Quote id + id: [16]u8, + /// Quote unit + unit: CurrencyUnit, + /// Quote amount + amount: amount_lib.Amount, + /// Quote Payment request e.g. bolt11 + request: []const u8, + /// Quote fee reserve + fee_reserve: amount_lib.Amount, + /// Quote state + state: MeltQuoteState, + /// Expiration time of quote + expiry: u64, + /// Payment preimage + payment_preimage: ?[]const u8, + /// Value used by ln backend to look up state of request + request_lookup_id: []const u8, + + /// Create new [`MeltQuote`] + pub fn init( + request: []const u8, + unit: CurrencyUnit, + amount: amount_lib.Amount, + fee_reserve: amount_lib.Amount, + expiry: u64, + request_lookup_id: []const u8, + ) MeltQuote { + const id = zul.UUID.v4(); + + return .{ + .id = id.bin, + .amount = amount, + .unit = unit, + .request = request, + .fee_reserve = fee_reserve, + .state = .unpaid, + .expiry = expiry, + .payment_preimage = null, + .request_lookup_id = request_lookup_id, + }; + } +}; diff --git a/src/mint/url.zig b/src/mint/url.zig new file mode 100644 index 0000000..64204cc --- /dev/null +++ b/src/mint/url.zig @@ -0,0 +1,17 @@ +const std = @import("std"); + +pub fn Url(comptime max_size: usize) type { + return struct { + inner: std.BoundedArray(u8, max_size), + + /// New mint url + pub fn new(s: []const u8) @This() { + var inner = try std.BoundedArray(u8, max_size).init(0); + inner.appendSlice(s) catch @panic("overflow"); + + return .{ + .inner = inner, + }; + } + }; +}