Skip to content

Commit

Permalink
feat: swap router
Browse files Browse the repository at this point in the history
  • Loading branch information
StringNick committed Sep 14, 2024
1 parent 4bacaf1 commit 89bda97
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 1 deletion.
241 changes: 241 additions & 0 deletions src/core/mint/mint.zig
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,247 @@ pub const Mint = struct {
.signatures = try blind_signatures.toOwnedSlice(),
};
}

/// Fee required for proof set
pub fn getProofsFee(self: *Mint, gpa: std.mem.Allocator, proofs: []const nuts.Proof) !core.amount.Amount {
var sum_fee: u64 = 0;

for (proofs) |proof| {
const input_fee_ppk = try self
.localstore
.value
.getKeysetInfo(gpa, proof.keyset_id) orelse return error.UnknownKeySet;
defer input_fee_ppk.deinit(gpa);

sum_fee += input_fee_ppk.input_fee_ppk;
}

const fee = (sum_fee + 999) / 1000;

return fee;
}

/// Check Tokens are not spent or pending
pub fn checkYsSpendable(
self: *Mint,
ys: []const secp256k1.PublicKey,
proof_state: nuts.nut07.State,
) !void {
const proofs_state = try self
.localstore
.value
.updateProofsStates(self.allocator, ys, proof_state);
defer proofs_state.deinit();

for (proofs_state.items) |p|
if (p) |proof| switch (proof) {
.pending => return error.TokenPending,
.spent => return error.TokenAlreadySpent,
else => continue,
};
}

/// Verify [`Proof`] meets conditions and is signed
pub fn verifyProof(self: *Mint, proof: nuts.Proof) !void {
// Check if secret is a nut10 secret with conditions
if (nuts.nut10.Secret.fromSecret(proof.secret, self.allocator)) |secret| {
defer secret.deinit();

// Checks and verifes known secret kinds.
// If it is an unknown secret kind it will be treated as a normal secret.
// Spending conditions will **not** be check. It is up to the wallet to ensure
// only supported secret kinds are used as there is no way for the mint to enforce
// only signing supported secrets as they are blinded at that point.

switch (secret.value.kind) {
.p2pk => try nuts.nut11.verifyP2pkProof(&proof, self.allocator),
.htlc => try nuts.verifyHTLC(&proof, self.allocator),
}
} else |_| {}

try self.ensureKeysetLoaded(proof.keyset_id);

const sec_key = v: {
self.keysets.lock.lock();
defer self.keysets.lock.unlock();
const keyset = self.keysets.value.get(proof.keyset_id) orelse return error.UnknownKeySet;

break :v (keyset.keys.inner.get(proof.amount) orelse return error.AmountKey).secret_key;
};

try core.dhke.verifyMessage(self.secp_ctx, sec_key, proof.c, proof.secret.toBytes());
}

/// Process Swap
/// expecting allocator as arena
pub fn processSwapRequest(
self: *Mint,
arena: std.mem.Allocator,
swap_request: nuts.SwapRequest,
) !nuts.SwapResponse {
var blinded_messages = try std.ArrayList(secp256k1.PublicKey).initCapacity(arena, swap_request.outputs.len);

for (swap_request.outputs) |b| {
blinded_messages.appendAssumeCapacity(b.blinded_secret);
}

const _blind_signatures = try self.localstore.value.getBlindSignatures(arena, blinded_messages.items);

for (_blind_signatures.items) |bs| {
if (bs != null) {
std.log.debug("output has already been signed", .{});

return error.BlindedMessageAlreadySigned;
}
}

const proofs_total = swap_request.inputAmount();
const output_total = swap_request.outputAmount();

const fee = try self.getProofsFee(arena, swap_request.inputs);

if (proofs_total < output_total + fee) {
std.log.info("Swap request without enough inputs: {}, outputs {}, fee {}", .{
proofs_total, output_total, fee,
});

return error.InsufficientInputs;
}

var input_ys = try std.ArrayList(secp256k1.PublicKey).initCapacity(arena, swap_request.inputs.len);

for (swap_request.inputs) |p| {
input_ys.appendAssumeCapacity(try core.dhke.hashToCurve(p.secret.toBytes()));
}

try self.localstore.value.addProofs(swap_request.inputs);
try self.checkYsSpendable(input_ys.items, .pending);

// Check that there are no duplicate proofs in request

{
var h = std.AutoHashMap(secp256k1.PublicKey, void).init(arena);

try h.ensureTotalCapacity(@intCast(input_ys.items.len));

for (input_ys.items) |i| {
if (h.fetchPutAssumeCapacity(i, {}) != null) {
_ = try self.localstore.value.updateProofsStates(arena, input_ys.items, .unspent);
return error.DuplicateProofs;
}
}
}

for (swap_request.inputs) |proof| {
self.verifyProof(proof) catch |err| {
std.log.info("Error verifying proof in swap", .{});
return err;
};
}

var input_keyset_ids = std.AutoHashMap(nuts.Id, void).init(arena);

try input_keyset_ids.ensureTotalCapacity(@intCast(swap_request.inputs.len));

for (swap_request.inputs) |p| input_keyset_ids.putAssumeCapacity(p.keyset_id, {});

var keyset_units = std.AutoHashMap(nuts.CurrencyUnit, void).init(arena);

{
var it = input_keyset_ids.keyIterator();

while (it.next()) |id| {
const keyset = try self.localstore.value.getKeysetInfo(arena, id.*) orelse {
std.log.debug("Swap request with unknown keyset in inputs", .{});
_ = try self.localstore.value.updateProofsStates(arena, input_ys.items, .unspent);
continue;
};

try keyset_units.put(keyset.unit, {});
}
}

var output_keyset_ids = std.AutoHashMap(nuts.Id, void).init(arena);

try output_keyset_ids.ensureTotalCapacity(@intCast(swap_request.outputs.len));
for (swap_request.outputs) |p| output_keyset_ids.putAssumeCapacity(p.keyset_id, {});

{
var it = output_keyset_ids.keyIterator();
while (it.next()) |id| {
const keyset = try self.localstore.value.getKeysetInfo(arena, id.*) orelse {
std.log.debug("Swap request with unknown keyset in outputs", .{});
_ = try self.localstore.value.updateProofsStates(arena, input_ys.items, .unspent);
continue;
};

keyset_units.putAssumeCapacity(keyset.unit, {});
}
}

// Check that all proofs are the same unit
// in the future it maybe possible to support multiple units but unsupported for
// now
if (keyset_units.count() > 1) {
std.log.err("Only one unit is allowed in request: {any}", .{keyset_units});

_ = try self.localstore
.value
.updateProofsStates(arena, input_ys.items, .unspent);

return error.MultipleUnits;
}

var enforced_sig_flag = try core.nuts.nut11.enforceSigFlag(arena, swap_request.inputs);

// let EnforceSigFlag {
// sig_flag,
// pubkeys,
// sigs_required,
// } = enforce_sig_flag(swap_request.inputs.clone());

if (enforced_sig_flag.sig_flag == .sig_all) {
var _pubkeys = try std.ArrayList(secp256k1.PublicKey).initCapacity(arena, enforced_sig_flag.pubkeys.count());

var it = enforced_sig_flag.pubkeys.keyIterator();

while (it.next()) |key| {
_pubkeys.appendAssumeCapacity(key.*);
}

for (swap_request.outputs) |*blinded_message| {
nuts.nut11.verifyP2pkBlindedMessages(blinded_message, _pubkeys.items, enforced_sig_flag.sigs_required) catch |err| {
std.log.info("Could not verify p2pk in swap request", .{});
_ = try self.localstore
.value
.updateProofsStates(arena, input_ys.items, .unspent);
return err;
};
}
}

var promises = try std.ArrayList(nuts.BlindSignature).initCapacity(arena, swap_request.outputs.len);

for (swap_request.outputs) |blinded_message| {
const blinded_signature = try self.blindSign(arena, blinded_message);
promises.appendAssumeCapacity(blinded_signature);
}

_ = try self.localstore
.value
.updateProofsStates(arena, input_ys.items, .spent);

try self.localstore
.value
.addBlindSignatures(
blinded_messages.items,
promises.items,
);

return .{
.signatures = promises.items,
};
}
};

/// Generate new [`MintKeySetInfo`] from path
Expand Down
62 changes: 61 additions & 1 deletion src/core/nuts/nut11/nut11.zig
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ pub fn signP2PKByProof(self: *Proof, allocator: std.mem.Allocator, secret_key: s
}

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

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

/// Enforce Sigflag info
pub const EnforceSigFlag = struct {
/// Sigflag required for proofs
sig_flag: SigFlag,
/// Pubkeys that can sign for proofs
pubkeys: std.AutoHashMap(secp256k1.PublicKey, void),
/// Number of sigs required for proofs
sigs_required: u64,

pub fn deinit(self: *EnforceSigFlag) void {
self.pubkeys.deinit();
}
};

/// Get the signature flag that should be enforced for a set of proofs and the public keys that signatures are valid for
pub fn enforceSigFlag(gpa: std.mem.Allocator, proofs: []const Proof) !EnforceSigFlag {
var sig_flag = SigFlag.sig_inputs;
var pubkeys = std.AutoHashMap(secp256k1.PublicKey, void).init(gpa);
errdefer pubkeys.deinit();

var sigs_required: usize = 1;

for (proofs) |proof| {
if (Nut10Secret.fromSecret(proof.secret, gpa)) |secret| {
defer secret.deinit();

if (secret.value.kind == .p2pk) {
try pubkeys.put(secp256k1.PublicKey.fromString(secret.value.secret_data.data) catch continue, {});
}

if (secret.value.secret_data.tags) |tags| {
const conditions = try Conditions.fromTags(tags, gpa);
defer conditions.deinit();

if (conditions.sig_flag == .sig_all) {
sig_flag = .sig_all;
}

if (conditions.num_sigs) |sigs| {
if (sigs > sigs_required) {
sigs_required = sigs;
}
}

if (conditions.pubkeys) |pubs| {
for (pubs.items) |p| {
try pubkeys.put(p, {});
}
}
}
} else |_| {}
}

return .{
.sig_flag = sig_flag,
.pubkeys = pubkeys,
.sigs_required = sigs_required,
};
}

test "test_secret_ser" {
const data = try secp256k1.PublicKey.fromString(
"033281c37677ea273eb7183b783067f5244933ef78d8c3f15b1a77cb246099c26e",
Expand Down
1 change: 1 addition & 0 deletions src/router/router.zig
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,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, .{});
router.post("/v1/swap", router_handlers.postSwap, .{});
return srv;
}

Expand Down
17 changes: 17 additions & 0 deletions src/router/router_handlers.zig
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,20 @@ pub fn getMeltBolt11Quote(
};
return try res.json(core.nuts.nut05.MeltQuoteBolt11Response.fromMeltQuote(quote), .{});
}

pub fn postSwap(
state: MintState,
req: *httpz.Request,
res: *httpz.Response,
) !void {
errdefer std.log.debug("{any}", .{@errorReturnTrace()});

const payload = (try req.json(core.nuts.SwapRequest)) orelse return error.WrongRequest;

const swap_response = state.mint.processSwapRequest(res.arena, payload) catch |err| {
std.log.err("Could not process swap request: {}", .{err});
return err;
};

return try res.json(swap_response, .{});
}

0 comments on commit 89bda97

Please sign in to comment.