Skip to content

Commit 89bda97

Browse files
committed
feat: swap router
1 parent 4bacaf1 commit 89bda97

File tree

4 files changed

+320
-1
lines changed

4 files changed

+320
-1
lines changed

src/core/mint/mint.zig

+241
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,247 @@ pub const Mint = struct {
696696
.signatures = try blind_signatures.toOwnedSlice(),
697697
};
698698
}
699+
700+
/// Fee required for proof set
701+
pub fn getProofsFee(self: *Mint, gpa: std.mem.Allocator, proofs: []const nuts.Proof) !core.amount.Amount {
702+
var sum_fee: u64 = 0;
703+
704+
for (proofs) |proof| {
705+
const input_fee_ppk = try self
706+
.localstore
707+
.value
708+
.getKeysetInfo(gpa, proof.keyset_id) orelse return error.UnknownKeySet;
709+
defer input_fee_ppk.deinit(gpa);
710+
711+
sum_fee += input_fee_ppk.input_fee_ppk;
712+
}
713+
714+
const fee = (sum_fee + 999) / 1000;
715+
716+
return fee;
717+
}
718+
719+
/// Check Tokens are not spent or pending
720+
pub fn checkYsSpendable(
721+
self: *Mint,
722+
ys: []const secp256k1.PublicKey,
723+
proof_state: nuts.nut07.State,
724+
) !void {
725+
const proofs_state = try self
726+
.localstore
727+
.value
728+
.updateProofsStates(self.allocator, ys, proof_state);
729+
defer proofs_state.deinit();
730+
731+
for (proofs_state.items) |p|
732+
if (p) |proof| switch (proof) {
733+
.pending => return error.TokenPending,
734+
.spent => return error.TokenAlreadySpent,
735+
else => continue,
736+
};
737+
}
738+
739+
/// Verify [`Proof`] meets conditions and is signed
740+
pub fn verifyProof(self: *Mint, proof: nuts.Proof) !void {
741+
// Check if secret is a nut10 secret with conditions
742+
if (nuts.nut10.Secret.fromSecret(proof.secret, self.allocator)) |secret| {
743+
defer secret.deinit();
744+
745+
// Checks and verifes known secret kinds.
746+
// If it is an unknown secret kind it will be treated as a normal secret.
747+
// Spending conditions will **not** be check. It is up to the wallet to ensure
748+
// only supported secret kinds are used as there is no way for the mint to enforce
749+
// only signing supported secrets as they are blinded at that point.
750+
751+
switch (secret.value.kind) {
752+
.p2pk => try nuts.nut11.verifyP2pkProof(&proof, self.allocator),
753+
.htlc => try nuts.verifyHTLC(&proof, self.allocator),
754+
}
755+
} else |_| {}
756+
757+
try self.ensureKeysetLoaded(proof.keyset_id);
758+
759+
const sec_key = v: {
760+
self.keysets.lock.lock();
761+
defer self.keysets.lock.unlock();
762+
const keyset = self.keysets.value.get(proof.keyset_id) orelse return error.UnknownKeySet;
763+
764+
break :v (keyset.keys.inner.get(proof.amount) orelse return error.AmountKey).secret_key;
765+
};
766+
767+
try core.dhke.verifyMessage(self.secp_ctx, sec_key, proof.c, proof.secret.toBytes());
768+
}
769+
770+
/// Process Swap
771+
/// expecting allocator as arena
772+
pub fn processSwapRequest(
773+
self: *Mint,
774+
arena: std.mem.Allocator,
775+
swap_request: nuts.SwapRequest,
776+
) !nuts.SwapResponse {
777+
var blinded_messages = try std.ArrayList(secp256k1.PublicKey).initCapacity(arena, swap_request.outputs.len);
778+
779+
for (swap_request.outputs) |b| {
780+
blinded_messages.appendAssumeCapacity(b.blinded_secret);
781+
}
782+
783+
const _blind_signatures = try self.localstore.value.getBlindSignatures(arena, blinded_messages.items);
784+
785+
for (_blind_signatures.items) |bs| {
786+
if (bs != null) {
787+
std.log.debug("output has already been signed", .{});
788+
789+
return error.BlindedMessageAlreadySigned;
790+
}
791+
}
792+
793+
const proofs_total = swap_request.inputAmount();
794+
const output_total = swap_request.outputAmount();
795+
796+
const fee = try self.getProofsFee(arena, swap_request.inputs);
797+
798+
if (proofs_total < output_total + fee) {
799+
std.log.info("Swap request without enough inputs: {}, outputs {}, fee {}", .{
800+
proofs_total, output_total, fee,
801+
});
802+
803+
return error.InsufficientInputs;
804+
}
805+
806+
var input_ys = try std.ArrayList(secp256k1.PublicKey).initCapacity(arena, swap_request.inputs.len);
807+
808+
for (swap_request.inputs) |p| {
809+
input_ys.appendAssumeCapacity(try core.dhke.hashToCurve(p.secret.toBytes()));
810+
}
811+
812+
try self.localstore.value.addProofs(swap_request.inputs);
813+
try self.checkYsSpendable(input_ys.items, .pending);
814+
815+
// Check that there are no duplicate proofs in request
816+
817+
{
818+
var h = std.AutoHashMap(secp256k1.PublicKey, void).init(arena);
819+
820+
try h.ensureTotalCapacity(@intCast(input_ys.items.len));
821+
822+
for (input_ys.items) |i| {
823+
if (h.fetchPutAssumeCapacity(i, {}) != null) {
824+
_ = try self.localstore.value.updateProofsStates(arena, input_ys.items, .unspent);
825+
return error.DuplicateProofs;
826+
}
827+
}
828+
}
829+
830+
for (swap_request.inputs) |proof| {
831+
self.verifyProof(proof) catch |err| {
832+
std.log.info("Error verifying proof in swap", .{});
833+
return err;
834+
};
835+
}
836+
837+
var input_keyset_ids = std.AutoHashMap(nuts.Id, void).init(arena);
838+
839+
try input_keyset_ids.ensureTotalCapacity(@intCast(swap_request.inputs.len));
840+
841+
for (swap_request.inputs) |p| input_keyset_ids.putAssumeCapacity(p.keyset_id, {});
842+
843+
var keyset_units = std.AutoHashMap(nuts.CurrencyUnit, void).init(arena);
844+
845+
{
846+
var it = input_keyset_ids.keyIterator();
847+
848+
while (it.next()) |id| {
849+
const keyset = try self.localstore.value.getKeysetInfo(arena, id.*) orelse {
850+
std.log.debug("Swap request with unknown keyset in inputs", .{});
851+
_ = try self.localstore.value.updateProofsStates(arena, input_ys.items, .unspent);
852+
continue;
853+
};
854+
855+
try keyset_units.put(keyset.unit, {});
856+
}
857+
}
858+
859+
var output_keyset_ids = std.AutoHashMap(nuts.Id, void).init(arena);
860+
861+
try output_keyset_ids.ensureTotalCapacity(@intCast(swap_request.outputs.len));
862+
for (swap_request.outputs) |p| output_keyset_ids.putAssumeCapacity(p.keyset_id, {});
863+
864+
{
865+
var it = output_keyset_ids.keyIterator();
866+
while (it.next()) |id| {
867+
const keyset = try self.localstore.value.getKeysetInfo(arena, id.*) orelse {
868+
std.log.debug("Swap request with unknown keyset in outputs", .{});
869+
_ = try self.localstore.value.updateProofsStates(arena, input_ys.items, .unspent);
870+
continue;
871+
};
872+
873+
keyset_units.putAssumeCapacity(keyset.unit, {});
874+
}
875+
}
876+
877+
// Check that all proofs are the same unit
878+
// in the future it maybe possible to support multiple units but unsupported for
879+
// now
880+
if (keyset_units.count() > 1) {
881+
std.log.err("Only one unit is allowed in request: {any}", .{keyset_units});
882+
883+
_ = try self.localstore
884+
.value
885+
.updateProofsStates(arena, input_ys.items, .unspent);
886+
887+
return error.MultipleUnits;
888+
}
889+
890+
var enforced_sig_flag = try core.nuts.nut11.enforceSigFlag(arena, swap_request.inputs);
891+
892+
// let EnforceSigFlag {
893+
// sig_flag,
894+
// pubkeys,
895+
// sigs_required,
896+
// } = enforce_sig_flag(swap_request.inputs.clone());
897+
898+
if (enforced_sig_flag.sig_flag == .sig_all) {
899+
var _pubkeys = try std.ArrayList(secp256k1.PublicKey).initCapacity(arena, enforced_sig_flag.pubkeys.count());
900+
901+
var it = enforced_sig_flag.pubkeys.keyIterator();
902+
903+
while (it.next()) |key| {
904+
_pubkeys.appendAssumeCapacity(key.*);
905+
}
906+
907+
for (swap_request.outputs) |*blinded_message| {
908+
nuts.nut11.verifyP2pkBlindedMessages(blinded_message, _pubkeys.items, enforced_sig_flag.sigs_required) catch |err| {
909+
std.log.info("Could not verify p2pk in swap request", .{});
910+
_ = try self.localstore
911+
.value
912+
.updateProofsStates(arena, input_ys.items, .unspent);
913+
return err;
914+
};
915+
}
916+
}
917+
918+
var promises = try std.ArrayList(nuts.BlindSignature).initCapacity(arena, swap_request.outputs.len);
919+
920+
for (swap_request.outputs) |blinded_message| {
921+
const blinded_signature = try self.blindSign(arena, blinded_message);
922+
promises.appendAssumeCapacity(blinded_signature);
923+
}
924+
925+
_ = try self.localstore
926+
.value
927+
.updateProofsStates(arena, input_ys.items, .spent);
928+
929+
try self.localstore
930+
.value
931+
.addBlindSignatures(
932+
blinded_messages.items,
933+
promises.items,
934+
);
935+
936+
return .{
937+
.signatures = promises.items,
938+
};
939+
}
699940
};
700941

701942
/// Generate new [`MintKeySetInfo`] from path

src/core/nuts/nut11/nut11.zig

+61-1
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ pub fn signP2PKByProof(self: *Proof, allocator: std.mem.Allocator, secret_key: s
391391
}
392392

393393
/// Verify P2PK signature on [Proof]
394-
pub fn verifyP2pkProof(self: *Proof, allocator: std.mem.Allocator) !void {
394+
pub fn verifyP2pkProof(self: *const Proof, allocator: std.mem.Allocator) !void {
395395
const secret = try Nut10Secret.fromSecret(self.secret, allocator);
396396
defer secret.deinit();
397397

@@ -502,6 +502,66 @@ pub fn verifyP2pkBlindedMessages(self: *const BlindedMessage, pubkeys: []const s
502502
return error.SpendConditionsNotMet;
503503
}
504504

505+
/// Enforce Sigflag info
506+
pub const EnforceSigFlag = struct {
507+
/// Sigflag required for proofs
508+
sig_flag: SigFlag,
509+
/// Pubkeys that can sign for proofs
510+
pubkeys: std.AutoHashMap(secp256k1.PublicKey, void),
511+
/// Number of sigs required for proofs
512+
sigs_required: u64,
513+
514+
pub fn deinit(self: *EnforceSigFlag) void {
515+
self.pubkeys.deinit();
516+
}
517+
};
518+
519+
/// Get the signature flag that should be enforced for a set of proofs and the public keys that signatures are valid for
520+
pub fn enforceSigFlag(gpa: std.mem.Allocator, proofs: []const Proof) !EnforceSigFlag {
521+
var sig_flag = SigFlag.sig_inputs;
522+
var pubkeys = std.AutoHashMap(secp256k1.PublicKey, void).init(gpa);
523+
errdefer pubkeys.deinit();
524+
525+
var sigs_required: usize = 1;
526+
527+
for (proofs) |proof| {
528+
if (Nut10Secret.fromSecret(proof.secret, gpa)) |secret| {
529+
defer secret.deinit();
530+
531+
if (secret.value.kind == .p2pk) {
532+
try pubkeys.put(secp256k1.PublicKey.fromString(secret.value.secret_data.data) catch continue, {});
533+
}
534+
535+
if (secret.value.secret_data.tags) |tags| {
536+
const conditions = try Conditions.fromTags(tags, gpa);
537+
defer conditions.deinit();
538+
539+
if (conditions.sig_flag == .sig_all) {
540+
sig_flag = .sig_all;
541+
}
542+
543+
if (conditions.num_sigs) |sigs| {
544+
if (sigs > sigs_required) {
545+
sigs_required = sigs;
546+
}
547+
}
548+
549+
if (conditions.pubkeys) |pubs| {
550+
for (pubs.items) |p| {
551+
try pubkeys.put(p, {});
552+
}
553+
}
554+
}
555+
} else |_| {}
556+
}
557+
558+
return .{
559+
.sig_flag = sig_flag,
560+
.pubkeys = pubkeys,
561+
.sigs_required = sigs_required,
562+
};
563+
}
564+
505565
test "test_secret_ser" {
506566
const data = try secp256k1.PublicKey.fromString(
507567
"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",

src/router/router.zig

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub fn createMintServer(
5656
router.post("/v1/mint/quote/bolt11", router_handlers.getMintBolt11Quote, .{});
5757
router.post("/v1/melt/quote/bolt11", router_handlers.getMeltBolt11Quote, .{});
5858
router.post("/v1/mint/bolt11", router_handlers.postMintBolt11, .{});
59+
router.post("/v1/swap", router_handlers.postSwap, .{});
5960
return srv;
6061
}
6162

src/router/router_handlers.zig

+17
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,20 @@ pub fn getMeltBolt11Quote(
173173
};
174174
return try res.json(core.nuts.nut05.MeltQuoteBolt11Response.fromMeltQuote(quote), .{});
175175
}
176+
177+
pub fn postSwap(
178+
state: MintState,
179+
req: *httpz.Request,
180+
res: *httpz.Response,
181+
) !void {
182+
errdefer std.log.debug("{any}", .{@errorReturnTrace()});
183+
184+
const payload = (try req.json(core.nuts.SwapRequest)) orelse return error.WrongRequest;
185+
186+
const swap_response = state.mint.processSwapRequest(res.arena, payload) catch |err| {
187+
std.log.err("Could not process swap request: {}", .{err});
188+
return err;
189+
};
190+
191+
return try res.json(swap_response, .{});
192+
}

0 commit comments

Comments
 (0)