From 3f3a9c2ae116380350dd447671507a82b0f7634e Mon Sep 17 00:00:00 2001 From: StringNick <stringnickq@gmail.com> Date: Sat, 7 Sep 2024 16:43:08 +0200 Subject: [PATCH] some mint tests + missing methods --- src/core/database/mint_memory.zig | 84 ++++++++- src/core/mint/mint.zig | 289 ++++++++++++++++++++++++++++++ src/core/nuts/nut01/nut01.zig | 4 + src/core/nuts/nut02/nut02.zig | 4 + src/core/nuts/nut06/nut06.zig | 2 +- src/mint.zig | 2 +- 6 files changed, 376 insertions(+), 9 deletions(-) diff --git a/src/core/database/mint_memory.zig b/src/core/database/mint_memory.zig index 2c33756..694aa60 100644 --- a/src/core/database/mint_memory.zig +++ b/src/core/database/mint_memory.zig @@ -21,12 +21,74 @@ pub const MintMemoryDatabase = struct { mint_quotes: std.AutoHashMap([16]u8, MintQuote), melt_quotes: std.AutoHashMap([16]u8, MeltQuote), proofs: std.AutoHashMap([33]u8, nuts.Proof), - proof_state: std.AutoHashMap([33]u8, nuts.nut07.State), + proof_states: std.AutoHashMap([33]u8, nuts.nut07.State), blinded_signatures: std.AutoHashMap([33]u8, nuts.BlindSignature), allocator: std.mem.Allocator, - pub fn init( + /// initFrom - take own on all data there, except slices (only own data in slices) + pub fn initFrom( + allocator: std.mem.Allocator, + active_keysets: std.AutoHashMap(nuts.CurrencyUnit, nuts.Id), + keysets: []const MintKeySetInfo, + mint_quotes: []const MintQuote, + melt_quotes: []const MeltQuote, + pending_proofs: []const nuts.Proof, + spent_proofs: []const nuts.Proof, + blinded_signatures: std.AutoHashMap([33]u8, nuts.BlindSignature), + ) !MintMemoryDatabase { + var proofs = std.AutoHashMap([33]u8, nuts.Proof).init(allocator); + errdefer proofs.deinit(); + + var proof_states = std.AutoHashMap([33]u8, nuts.nut07.State).init(allocator); + errdefer proof_states.deinit(); + + for (pending_proofs) |proof| { + const y = (try dhke.hashToCurve(proof.secret.toBytes())).serialize(); + try proofs.put(y, proof); + try proof_states.put(y, .pending); + } + + for (spent_proofs) |proof| { + const y = (try dhke.hashToCurve(proof.secret.toBytes())).serialize(); + try proofs.put(y, proof); + try proof_states.put(y, .pending); + } + + var _keysets = std.AutoHashMap(nuts.Id, MintKeySetInfo).init(allocator); + errdefer _keysets.deinit(); + + for (keysets) |ks| { + try _keysets.put(ks.id, ks); + } + + var _mint_quotes = std.AutoHashMap([16]u8, MintQuote).init(allocator); + errdefer _mint_quotes.deinit(); + + for (mint_quotes) |q| { + try _mint_quotes.put(q.id, q); + } + var _melt_quotes = std.AutoHashMap([16]u8, MeltQuote).init(allocator); + errdefer _melt_quotes.deinit(); + + for (melt_quotes) |q| { + try _melt_quotes.put(q.id, q); + } + + return .{ + .allocator = allocator, + .lock = .{}, + .active_keysets = active_keysets, + .keysets = _keysets, + .mint_quotes = _mint_quotes, + .melt_quotes = _melt_quotes, + .proofs = proofs, + .proof_states = proof_states, + .blinded_signatures = blinded_signatures, + }; + } + + pub fn initManaged( allocator: std.mem.Allocator, ) !zul.Managed(Self) { var arena = try allocator.create(std.heap.ArenaAllocator); @@ -57,7 +119,7 @@ pub const MintMemoryDatabase = struct { .mint_quotes = mint_quotes, .melt_quotes = melt_quotes, .proofs = proofs, - .proof_state = proof_state, + .proof_states = proof_state, .blinded_signatures = blinded_signatures, .allocator = arena.allocator(), }, @@ -79,6 +141,14 @@ pub const MintMemoryDatabase = struct { return self.active_keysets.get(unit); } + /// caller own result data, so responsible to deallocate + pub fn getActiveKeysets(self: *Self, allocator: std.mem.Allocator) !std.AutoHashMap(nuts.CurrencyUnit, nuts.Id) { + self.lock.lockShared(); + defer self.lock.unlockShared(); + // key and value doesnt have heap data, so we can clone all map + return try self.active_keysets.cloneWithAllocator(allocator); + } + /// keyset inside is cloned, so caller own keyset pub fn addKeysetInfo(self: *Self, keyset: MintKeySetInfo) !void { self.lock.lock(); @@ -359,7 +429,7 @@ pub const MintMemoryDatabase = struct { errdefer states.deinit(); for (ys) |y| { - const kv = try self.proof_state.fetchPut(y.serialize(), proof_state); + const kv = try self.proof_states.fetchPut(y.serialize(), proof_state); states.appendAssumeCapacity(if (kv) |_kv| _kv.value else null); } @@ -375,7 +445,7 @@ pub const MintMemoryDatabase = struct { errdefer states.deinit(); for (ys) |y| { - states.appendAssumeCapacity(self.proof_state.get(y.serialize())); + states.appendAssumeCapacity(self.proof_states.get(y.serialize())); } return states; @@ -508,7 +578,7 @@ pub fn worker(m: *MintMemoryDatabase, unit: nuts.CurrencyUnit) !void { } test MintMemoryDatabase { - var db_arened = try MintMemoryDatabase.init(std.testing.allocator); + var db_arened = try MintMemoryDatabase.initManaged(std.testing.allocator); defer db_arened.deinit(); var db = &db_arened.value; var rnd = std.Random.DefaultPrng.init(std.testing.random_seed); @@ -563,7 +633,7 @@ test MintMemoryDatabase { } test "multithread" { - var shared_data = try MintMemoryDatabase.init(std.testing.allocator); + var shared_data = try MintMemoryDatabase.initManaged(std.testing.allocator); defer shared_data.deinit(); const thread1 = try std.Thread.spawn(.{ diff --git a/src/core/mint/mint.zig b/src/core/mint/mint.zig index 3ce4d20..1b0f389 100644 --- a/src/core/mint/mint.zig +++ b/src/core/mint/mint.zig @@ -11,6 +11,7 @@ const MintInfo = core.nuts.MintInfo; const MintQuoteBolt11Response = core.nuts.nut04.MintQuoteBolt11Response; const MintQuoteState = core.nuts.nut04.QuoteState; const MintKeySet = core.nuts.MintKeySet; +const CurrencyUnit = core.nuts.CurrencyUnit; pub const MintQuote = @import("types.zig").MintQuote; pub const MeltQuote = @import("types.zig").MeltQuote; @@ -109,6 +110,104 @@ pub const Mint = struct { if (keyset_infos.value.items.len > 0) { std.log.debug("setting all keysets to inactive, size = {d}", .{keyset_infos.value.items.len}); // TODO this part of code + + for (keyset_infos.value.items) |keyset| { + // Set all to in active + var ks = keyset; + ks.active = false; + + try localstore.addKeysetInfo(ks); + } + + const keysets_by_unit = v: { + // allocating through arena for easy deallocation + var result = std.AutoHashMap(CurrencyUnit, std.ArrayList(MintKeySetInfo)).init(keyset_infos.arena.allocator()); + + for (keyset_infos.value.items) |ks| { + var gop = try result.getOrPut(ks.unit); + if (!gop.found_existing) { + // already exist + gop.value_ptr.* = std.ArrayList(MintKeySetInfo).init(keyset_infos.arena.allocator()); + } + + try gop.value_ptr.append(ks); + } + + break :v result; + }; + + var it = keysets_by_unit.iterator(); + + while (it.next()) |entry| { + const unit = entry.key_ptr.*; + const _keysets = entry.value_ptr.*; + + std.sort.block(MintKeySetInfo, _keysets.items, {}, (struct { + fn compare(_: void, l: MintKeySetInfo, r: MintKeySetInfo) bool { + if (r.derivation_path_index) |r_dpi| { + if (l.derivation_path_index) |l_dpi| { + if (l_dpi < r_dpi) return true; + } else return true; + } + + // other all cases false + return false; + } + }).compare); + + const highest_index_keyset = _keysets.getLast(); + + var keysets = try std.ArrayList(MintKeySetInfo).initCapacity(keyset_infos.arena.allocator(), _keysets.items.len); + + for (_keysets.items) |ks| { + if (ks.derivation_path_index != null) keysets.appendAssumeCapacity(ks); + } + + if (supported_units.get(unit)) |supp_unit| { + const input_fee_ppk, const max_order = supp_unit; + + const derivation_path_index: u32 = if (keysets.items.len == 0) + 1 + else if (highest_index_keyset.input_fee_ppk == input_fee_ppk and highest_index_keyset.max_order == max_order) { + const id = highest_index_keyset.id; + const keyset = try MintKeySet.generateFromXpriv( + keyset_infos.arena.allocator(), + secp_ctx, + xpriv, + highest_index_keyset.max_order, + highest_index_keyset.unit, + highest_index_keyset.derivation_path, + ); + + try active_keysets.put(id, keyset); + var keyset_info = highest_index_keyset; + keyset_info.active = true; + + try localstore.addKeysetInfo(keyset_info); + try localstore.setActiveKeyset(unit, id); + continue; + } else highest_index_keyset.derivation_path_index orelse 0 + 1; + + const derivation_path = derivationPathFromUnit(unit, derivation_path_index); + + const keyset, const keyset_info = try createNewKeysetAlloc( + keyset_infos.arena.allocator(), + secp_ctx, + xpriv, + &derivation_path, + derivation_path_index, + unit, + max_order, + input_fee_ppk, + ); + + const id = keyset_info.id; + try localstore.addKeysetInfo(keyset_info); + try localstore.setActiveKeyset(unit, id); + try active_keysets.put(id, keyset); + try active_keyset_units.append(unit); + } + } } var it = supported_units.iterator(); @@ -294,6 +393,71 @@ pub const Mint = struct { keyset_info.derivation_path, ); } + + /// Return a list of all supported keysets + /// caller responsible to deallocate result + pub fn getKeysets(self: *Mint, allocator: std.mem.Allocator) !core.nuts.KeysetResponse { + const keysets = try self.localstore.value.getKeysetInfos(allocator); + defer keysets.deinit(); + + var _active_keysets = try self.localstore.value.getActiveKeysets(allocator); + defer _active_keysets.deinit(); + + var active_keysets = std.AutoHashMap(core.nuts.Id, void).init(allocator); + defer active_keysets.deinit(); + + var it = _active_keysets.valueIterator(); + while (it.next()) |value| try active_keysets.put(value.*, {}); + + var result = try std.ArrayList(core.nuts.KeySetInfo).initCapacity(allocator, keysets.value.items.len); + errdefer result.deinit(); + + for (keysets.value.items) |ks| { + result.appendAssumeCapacity(.{ + .id = ks.id, + .unit = ks.unit, + .active = active_keysets.contains(ks.id), + .input_fee_ppk = ks.input_fee_ppk, + }); + } + + return .{ + .keysets = try result.toOwnedSlice(), + }; + } + + /// Add current keyset to inactive keysets + /// Generate new keyset + pub fn rotateKeyset( + self: *Mint, + allocator: std.mem.Allocator, + unit: nuts.CurrencyUnit, + derivation_path_index: u32, + max_order: u8, + input_fee_ppk: u64, + ) !void { + const derivation_path = derivationPathFromUnit(unit, derivation_path_index); + var keyset, const keyset_info = try createNewKeysetAlloc( + allocator, + self.secp_ctx, + self.xpriv, + &derivation_path, + derivation_path_index, + unit, + max_order, + input_fee_ppk, + ); + defer keyset_info.deinit(allocator); + defer keyset.deinit(); + + const id = keyset_info.id; + try self.localstore.value.addKeysetInfo(keyset_info); + try self.localstore.value.setActiveKeyset(unit, id); + + self.keysets.lock.lock(); + defer self.keysets.lock.unlock(); + try self.keysets.value.put(id, keyset); + } }; /// Generate new [`MintKeySetInfo`] from path @@ -338,3 +502,128 @@ fn derivationPathFromUnit(unit: core.nuts.CurrencyUnit, index: u32) [3]bip32.Chi bip32.ChildNumber.fromHardenedIdx(index) catch @panic("0 is a valid index"), }; } + +const expectEqual = std.testing.expectEqual; + +test "mint mod generate keyset from seed" { + const seed = "test_seed"; + + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + var keyset = try MintKeySet.generateFromSeed( + std.testing.allocator, + secp, + seed, + 2, + .sat, + &derivationPathFromUnit(.sat, 0), + ); + defer keyset.deinit(); + + try expectEqual(.sat, keyset.unit); + try expectEqual(2, keyset.keys.inner.count()); + + try expectEqual(try secp256k1.PublicKey.fromString("0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e"), keyset.keys.inner.get(1).?.public_key); + try expectEqual(try secp256k1.PublicKey.fromString("03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e"), keyset.keys.inner.get(2).?.public_key); +} + +test "mint mod generate keyset from xpriv" { + const seed = "test_seed"; + + var secp = try secp256k1.Secp256k1.genNew(); + defer secp.deinit(); + + const xpriv = try bip32.ExtendedPrivKey.initMaster(.MAINNET, seed); + + var keyset = try MintKeySet.generateFromXpriv( + std.testing.allocator, + secp, + xpriv, + 2, + .sat, + &derivationPathFromUnit(.sat, 0), + ); + defer keyset.deinit(); + + try expectEqual(.sat, keyset.unit); + try expectEqual(2, keyset.keys.inner.count()); + + try expectEqual(try secp256k1.PublicKey.fromString("0257aed43bf2c1cdbe3e7ae2db2b27a723c6746fc7415e09748f6847916c09176e"), keyset.keys.inner.get(1).?.public_key); + try expectEqual(try secp256k1.PublicKey.fromString("03ad95811e51adb6231613f9b54ba2ba31e4442c9db9d69f8df42c2b26fbfed26e"), keyset.keys.inner.get(2).?.public_key); +} + +const MintConfig = struct { + active_keysets: std.AutoHashMap(CurrencyUnit, core.nuts.Id), + keysets: []const MintKeySetInfo = &.{}, + mint_quotes: []const MintQuote = &.{}, + melt_quotes: []const MeltQuote = &.{}, + pending_proofs: []const core.nuts.Proof = &.{}, + spent_proofs: []const core.nuts.Proof = &.{}, + blinded_signatures: std.AutoHashMap([33]u8, core.nuts.BlindSignature), + mint_url: []const u8 = &.{}, + seed: []const u8 = &.{}, + mint_info: MintInfo = .{}, + supported_units: std.AutoHashMap(CurrencyUnit, std.meta.Tuple(&.{ u64, u8 })), +}; + +fn createMint(arena: std.mem.Allocator, config: MintConfig) !Mint { + const db_ptr = try arena.create(MintMemoryDatabase); + db_ptr.* = try MintMemoryDatabase.initFrom(arena, config.active_keysets, config.keysets, config.mint_quotes, config.melt_quotes, config.pending_proofs, config.spent_proofs, config.blinded_signatures); + + return Mint.init( + arena, + config.mint_url, + config.seed, + config.mint_info, + db_ptr, + config.supported_units, + ); +} + +test "mint mod rotate keyset" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + const allocator = arena.allocator(); + + const config = MintConfig{ + .active_keysets = std.AutoHashMap(CurrencyUnit, core.nuts.Id).init(allocator), + .blinded_signatures = std.AutoHashMap([33]u8, core.nuts.BlindSignature).init(allocator), + .supported_units = std.AutoHashMap(CurrencyUnit, std.meta.Tuple(&.{ u64, u8 })).init(allocator), + }; + + var mint = try createMint(allocator, config); + + // dont deallocate due arena allocator + var keysets = try mint.getKeysets(allocator); + + try expectEqual(0, keysets.keysets.len); + + // generate the first keyset and set it to active + try mint.rotateKeyset(allocator, .sat, 0, 1, 1); + + // dont deallocate due arena allocator + keysets = try mint.getKeysets(allocator); + + try expectEqual(1, keysets.keysets.len); + try expectEqual(true, keysets.keysets[0].active); + + const first_keyset_id = keysets.keysets[0].id; + + // set the first keyset to inactive and generate a new keyset + try mint.rotateKeyset(allocator, .sat, 1, 1, 1); + + // dont deallocate due arena allocator + keysets = try mint.getKeysets(allocator); + + try expectEqual(2, keysets.keysets.len); + + for (keysets.keysets) |keyset| { + if (std.meta.eql(keyset.id, first_keyset_id)) { + try expectEqual(false, keyset.active); + } else { + try expectEqual(true, keyset.active); + } + } +} diff --git a/src/core/nuts/nut01/nut01.zig b/src/core/nuts/nut01/nut01.zig index 8274c54..88c3455 100644 --- a/src/core/nuts/nut01/nut01.zig +++ b/src/core/nuts/nut01/nut01.zig @@ -121,6 +121,10 @@ pub const MintKeys = struct { MintKeyPair, ), + pub fn deinit(self: *MintKeys) void { + self.inner.deinit(); + } + /// Create new [`MintKeys`] pub inline fn initFrom(allocator: std.mem.Allocator, map: std.AutoHashMap(u64, MintKeyPair)) !MintKeys { return .{ diff --git a/src/core/nuts/nut02/nut02.zig b/src/core/nuts/nut02/nut02.zig index e4502a1..6389f3c 100644 --- a/src/core/nuts/nut02/nut02.zig +++ b/src/core/nuts/nut02/nut02.zig @@ -166,6 +166,10 @@ pub const MintKeySet = struct { /// Keyset [`MintKeys`] keys: MintKeys, + pub fn deinit(self: *MintKeySet) void { + self.keys.deinit(); + } + pub fn toKeySet(self: MintKeySet, arena: std.mem.Allocator) !KeySet { return .{ .id = self.id, diff --git a/src/core/nuts/nut06/nut06.zig b/src/core/nuts/nut06/nut06.zig index 2a04f47..2f179ed 100644 --- a/src/core/nuts/nut06/nut06.zig +++ b/src/core/nuts/nut06/nut06.zig @@ -110,7 +110,7 @@ pub const MintInfo = struct { /// Contact info contact: ?[]const ContactInfo = null, /// shows which NUTs the mint supports - nuts: Nuts, + nuts: Nuts = .{}, /// Mint's icon URL mint_icon_url: ?[]const u8 = null, /// message of the day that the wallet must display to the user diff --git a/src/mint.zig b/src/mint.zig index a046d96..b362677 100644 --- a/src/mint.zig +++ b/src/mint.zig @@ -25,7 +25,7 @@ pub fn main() !void { var supported_units = std.AutoHashMap(core.nuts.CurrencyUnit, std.meta.Tuple(&.{ u64, u8 })).init(gpa.allocator()); defer supported_units.deinit(); - var db = try MintDatabase.init(gpa.allocator()); + var db = try MintDatabase.initManaged(gpa.allocator()); defer db.deinit(); var mint = try Mint.init(gpa.allocator(), "MintUrl", &try mnemonic.toSeedNormalized(&.{}), .{