diff --git a/src/core/mint/mint.zig b/src/core/mint/mint.zig index be79feb..c003ed7 100644 --- a/src/core/mint/mint.zig +++ b/src/core/mint/mint.zig @@ -422,7 +422,7 @@ pub const Mint = struct { // check if keyset already in { self.keysets.lock.lockShared(); - defer self.keysets.lock.lockShared(); + defer self.keysets.lock.unlockShared(); if (self.keysets.value.contains(id)) return; } @@ -587,6 +587,115 @@ pub const Mint = struct { _ = try self.localstore.value.updateMintQuoteState(mint_quote.id, .paid); } + + /// Blind Sign + pub fn blindSign( + self: *Mint, + gpa: std.mem.Allocator, + blinded_message: core.nuts.BlindedMessage, + ) !core.nuts.BlindSignature { + try self.ensureKeysetLoaded(blinded_message.keyset_id); + + const keyset_info = try self + .localstore.value + .getKeysetInfo(gpa, blinded_message.keyset_id) orelse return error.UnknownKeySet; + errdefer keyset_info.deinit(gpa); + + const active = self + .localstore + .value + .getActiveKeysetId(keyset_info.unit) orelse return error.InactiveKeyset; + + // Check that the keyset is active and should be used to sign + if (!std.meta.eql(keyset_info.id, active)) { + return error.InactiveKeyset; + } + + const keyset = v: { + self.keysets.lock.lock(); + defer self.keysets.lock.unlock(); + + break :v self.keysets.value.get(blinded_message.keyset_id) orelse return error.UknownKeySet; + }; + + const key_pair = keyset.keys.inner.get(blinded_message.amount) orelse return error.AmountKey; + + const c = try core.dhke.signMessage( + self.secp_ctx, + key_pair.secret_key, + blinded_message.blinded_secret, + ); + + const blinded_signature = try core.nuts.initBlindSignature( + self.secp_ctx, + blinded_message.amount, + c, + keyset_info.id, + blinded_message.blinded_secret, + key_pair.secret_key, + ); + + return blinded_signature; + } + + /// Process mint request + pub fn processMintRequest( + self: *Mint, + gpa: std.mem.Allocator, + mint_request: core.nuts.nut04.MintBolt11Request, + ) !core.nuts.nut04.MintBolt11Response { + const quote_id = try zul.UUID.parse(&mint_request.quote); + const state = try self.localstore.value.updateMintQuoteState(quote_id.bin, .pending); + + std.log.debug("process_mitn_request: quote_id {s}", .{quote_id.toHex(.lower)}); + switch (state) { + .unpaid => return error.UnpaidQuote, + .pending => return error.PendingQuote, + .issued => return error.IssuedQuote, + .paid => {}, + } + + var blinded_messages = try std.ArrayList(secp256k1.PublicKey).initCapacity(gpa, mint_request.outputs.len); + errdefer blinded_messages.deinit(); + + for (mint_request.outputs) |b| { + blinded_messages.appendAssumeCapacity(b.blinded_secret); + } + + const _blind_signatures = try self.localstore.value.getBlindSignatures(gpa, blinded_messages.items); + defer _blind_signatures.deinit(); + + for (_blind_signatures.items) |bs| { + if (bs != null) { + std.log.debug("output has already been signed", .{}); + std.log.debug("Mint {x} did not succeed returning quote to Paid state", .{mint_request.quote}); + + _ = try self.localstore + .value.updateMintQuoteState(quote_id.bin, .paid); + return error.BlindedMessageAlreadySigned; + } + } + + var blind_signatures = try std.ArrayList(core.nuts.BlindSignature).initCapacity(gpa, mint_request.outputs.len); + errdefer blind_signatures.deinit(); + + for (mint_request.outputs) |blinded_message| { + const blind_signature = try self.blindSign(gpa, blinded_message); + blind_signatures.appendAssumeCapacity(blind_signature); + } + + try self.localstore + .value + .addBlindSignatures(blinded_messages.items, blind_signatures.items); + + _ = try self.localstore.value.updateMintQuoteState(quote_id.bin, .issued); + + std.log.debug("process_mint_request: issued {s}", .{quote_id.toHex(.lower)}); + + return .{ + .signatures = try blind_signatures.toOwnedSlice(), + }; + } }; /// Generate new [`MintKeySetInfo`] from path diff --git a/src/core/mint/types.zig b/src/core/mint/types.zig index cf7aec7..d82b2f7 100644 --- a/src/core/mint/types.zig +++ b/src/core/mint/types.zig @@ -26,6 +26,16 @@ pub const MintQuote = struct { /// Value used by ln backend to look up state of request request_lookup_id: []const u8, + /// formatting mint quote + pub fn format( + self: MintQuote, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.print("{}", .{std.json.fmt(self, .{})}); + } + /// Create new [`MintQuote`] /// creating copy of arguments, so caller responsible on deinit resources pub fn initAlloc( diff --git a/src/core/nuts/nut04/nut04.zig b/src/core/nuts/nut04/nut04.zig index c7e676e..d267974 100644 --- a/src/core/nuts/nut04/nut04.zig +++ b/src/core/nuts/nut04/nut04.zig @@ -5,6 +5,8 @@ const std = @import("std"); const zul = @import("zul"); const CurrencyUnit = @import("../nut00/nut00.zig").CurrencyUnit; +const BlindedMessage = @import("../nut00/nut00.zig").BlindedMessage; +const BlindSignature = @import("../nut00/nut00.zig").BlindSignature; const Proof = @import("../nut00/nut00.zig").Proof; const PaymentMethod = @import("../nut00/nut00.zig").PaymentMethod; const MintQuote = @import("../../mint/types.zig").MintQuote; @@ -191,3 +193,17 @@ pub const MintQuoteBolt11Response = struct { }; } }; + +/// Mint response [NUT-04] +pub const MintBolt11Response = struct { + /// Blinded Signatures + signatures: []const BlindSignature, +}; + +/// Mint request [NUT-04] +pub const MintBolt11Request = struct { + /// Quote id + quote: [36]u8, + /// Outputs + outputs: []const BlindedMessage, +}; diff --git a/src/router/router.zig b/src/router/router.zig index 69b3a28..d6d3cdd 100644 --- a/src/router/router.zig +++ b/src/router/router.zig @@ -55,6 +55,7 @@ pub fn createMintServer( router.post("/v1/mint/quote/bolt11", router_handlers.getMintBolt11Quote, .{}); router.post("/v1/melt/quote/bolt11", router_handlers.getMeltBolt11Quote, .{}); + router.post("/v1/mint/bolt11", router_handlers.postMintBolt11, .{}); return srv; } diff --git a/src/router/router_handlers.zig b/src/router/router_handlers.zig index 09afbe4..6bb725a 100644 --- a/src/router/router_handlers.zig +++ b/src/router/router_handlers.zig @@ -116,6 +116,26 @@ pub fn getMintBolt11Quote( ); } +pub fn postMintBolt11( + state: MintState, + req: *httpz.Request, + res: *httpz.Response, +) !void { + errdefer std.log.debug("{any}", .{@errorReturnTrace()}); + + const payload = try req.json(core.nuts.nut04.MintBolt11Request) orelse return error.WrongRequest; + + const r = state.mint + .processMintRequest(res.arena, payload) catch |err| { + // TODO print self error + std.log.err("could not process mint {any}, err {any}", .{ payload, err }); + + return err; + }; + + return try res.json(r, .{}); +} + pub fn getMeltBolt11Quote( state: MintState, req: *httpz.Request,