Skip to content
This repository has been archived by the owner on Nov 21, 2024. It is now read-only.

Commit

Permalink
chore: move NUT01 code it it's own folder
Browse files Browse the repository at this point in the history
  • Loading branch information
tdelabro committed Oct 14, 2024
1 parent db8197a commit eeda98f
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 147 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
run: mix format --check-formatted

- name: Run Credo
run: mix credo --strict --ignore todo
run: mix credo --ignore todo

- name: Check for security vulnerabilities
run: mix sobelow --config
8 changes: 8 additions & 0 deletions lib/cashubrew/NUTs/NUT-00/models.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ defmodule Cashubrew.Nuts.Nut00.BlindedMessage do
def new_blinded_message(amount, id, B_) do
%__MODULE__{amount: amount, id: id, B_: B_}
end

def from_map(map) do
%__MODULE__{
amount: Map.fetch!(map, "amount"),
id: Map.fetch!(map, "id"),
B_: Map.fetch!(map, "B_")
}
end
end

defmodule Cashubrew.Nuts.Nut00.BlindSignature do
Expand Down
71 changes: 71 additions & 0 deletions lib/cashubrew/NUTs/NUT-01/keysets.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule Cashubrew.Nuts.Nut01.Keyset do
@moduledoc """
Contains the logic required for the Mint to generate keys and keysets
"""
alias Cashubrew.Nuts.Nut00.BDHKE

import Bitwise

defp max_order do
64
end

@doc """
Generates a keyset from a seed and derivation path.
## Parameters
- seed: A binary representing the seed.
- derivation_path: A string representing the derivation path (e.g., "m/0'/0'/0'").
## Returns
- A map containing amounts mapped to their corresponding private and public keys.
"""
def generate_keys(seed, derivation_path \\ "m/0'/0'/0'") do
encoded_seed =
seed
|> :unicode.characters_to_binary(:utf8)
|> Base.encode16(case: :lower)

{master_private_key, master_chain_code} = BlockKeys.CKD.master_keys(encoded_seed)
root_key = BlockKeys.CKD.master_private_key({master_private_key, master_chain_code})

Enum.map(0..(max_order() - 1), fn i ->
amount = 1 <<< i
child_path = "#{derivation_path}/#{i}"
child_key = BlockKeys.CKD.derive(root_key, child_path)

private_key = BlockKeys.Encoding.decode_extended_key(child_key)
private_key_bytes = :binary.part(private_key.key, 1, 32)

{_, public_key} = BDHKE.generate_keypair(private_key_bytes)

%{
amount: amount,
private_key: private_key_bytes,
public_key: public_key
}
end)
end

@spec derive_keyset_id([map()]) :: <<_::16, _::_*8>>
@doc """
Derives a keyset ID from a set of public keys.
## Parameters
- keys: A list of maps containing public keys.
## Returns
- A string representing the keyset ID.
"""
def derive_keyset_id(keys) do
sorted_keys = Enum.sort_by(keys, & &1.amount)
pubkeys_concat = Enum.map(sorted_keys, & &1.public_key) |> IO.iodata_to_binary()

"00" <>
(:crypto.hash(:sha256, pubkeys_concat) |> Base.encode16(case: :lower) |> binary_part(0, 14))
end
end
12 changes: 12 additions & 0 deletions lib/cashubrew/NUTs/NUT-01/routes.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Cashubrew.Nuts.Nut01.Routes do
@moduledoc """
List the rest routes defined in the NUT-01
"""

@doc """
The route to query active keys from the Mint
"""
def v1_keys do
"/v1/keys"
end
end
73 changes: 73 additions & 0 deletions lib/cashubrew/NUTs/NUT-01/serde.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
defmodule Cashubrew.Nuts.Nut01.Serde.GetKeysResponse do
@moduledoc """
The body of the get keys rest request
"""
@enforce_keys [:keysets]
defstruct [:keysets]

def from_keysets(keysets) do
%__MODULE__{keysets: inner_from_keysets(keysets, [])}
end

defp inner_from_keysets([], accumulator) do
accumulator
end

defp inner_from_keysets([%{id: id, unit: unit, keys: keys}, tail], accumulator) do
inner_from_keysets(tail, [
Cashubrew.Nuts.Nut01.Serde.Keyset.from_keyset(id, unit, keys) | accumulator
])
end
end

defmodule Cashubrew.Nuts.Nut01.Serde.Keys do
@moduledoc """
Keys for the Cashubrew mint.
"""
defstruct [:pairs]
end

defimpl Jason.Encoder, for: Cashubrew.Nuts.Nut01.Serde.Keys do
def encode(%Cashubrew.Nuts.Nut01.Keys{pairs: pairs}, opts) do
# Convert amounts to strings and sort them in ascending order
sorted_pairs =
pairs
|> Enum.map(fn {amount, pubkey} -> {to_string(amount), pubkey} end)
|> Enum.sort_by(
fn {amount_str, _} ->
{amount_int, ""} = Integer.parse(amount_str)
amount_int
end,
:asc
)

# Encode the sorted pairs as JSON
json_pairs =
sorted_pairs
|> Enum.map(fn {key, value} ->
[Jason.Encode.string(key, opts), ?:, Jason.Encode.string(value, opts)]
end)

json_pairs = Enum.intersperse(json_pairs, ?,)
["{", json_pairs, "}"]
end
end

defmodule Cashubrew.Nuts.Nut01.Serde.Keyset do
@moduledoc """
A keyset
"""
@enforce_keys [:id, :unit, :keys]
defstruct [:id, :unit, :keys]

def from_keyset(id, unit, keys) do
keys_list =
Enum.map(keys, fn key -> {key.amount, Base.encode16(key.public_key, case: :lower)} end)

%__MODULE__{
id: id,
unit: unit,
keys: %Cashubrew.Nuts.Nut01.Serde.Keys{pairs: keys_list}
}
end
end
71 changes: 3 additions & 68 deletions lib/cashubrew/schema/keyset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@ defmodule Cashubrew.Schema.Keyset do
"""
use Ecto.Schema
import Ecto.Changeset
alias Cashubrew.Nuts.Nut00.BDHKE
alias Cashubrew.Nuts.Nut01.Keyset
alias Cashubrew.Schema

import Bitwise

defp max_order do
64
end

@primary_key {:id, :string, autogenerate: false}
schema "keysets" do
field(:active, :boolean, default: true)
Expand All @@ -28,8 +22,8 @@ defmodule Cashubrew.Schema.Keyset do
end

def generate(unit \\ "sat", seed, derivation_path \\ "m/0'/0'/0'", input_fee_ppk \\ 0) do
keys = generate_keys(seed, derivation_path)
id = derive_keyset_id(keys)
keys = Keyset.generate_keys(seed, derivation_path)
id = Keyset.derive_keyset_id(keys)

keyset = %__MODULE__{
id: id,
Expand Down Expand Up @@ -67,63 +61,4 @@ defmodule Cashubrew.Schema.Keyset do
{:error, changeset} -> raise "Failed to insert key: #{inspect(changeset.errors)}"
end
end

@doc """
Generates a keyset from a seed and derivation path.
## Parameters
- seed: A binary representing the seed.
- derivation_path: A string representing the derivation path (e.g., "m/0'/0'/0'").
## Returns
- A map containing amounts mapped to their corresponding private and public keys.
"""
def generate_keys(seed, derivation_path \\ "m/0'/0'/0'") do
encoded_seed =
seed
|> :unicode.characters_to_binary(:utf8)
|> Base.encode16(case: :lower)

{master_private_key, master_chain_code} = BlockKeys.CKD.master_keys(encoded_seed)
root_key = BlockKeys.CKD.master_private_key({master_private_key, master_chain_code})

Enum.map(0..(max_order() - 1), fn i ->
amount = 1 <<< i
child_path = "#{derivation_path}/#{i}"
child_key = BlockKeys.CKD.derive(root_key, child_path)

private_key = BlockKeys.Encoding.decode_extended_key(child_key)
private_key_bytes = :binary.part(private_key.key, 1, 32)

{_, public_key} = BDHKE.generate_keypair(private_key_bytes)

%{
amount: amount,
private_key: private_key_bytes,
public_key: public_key
}
end)
end

@spec derive_keyset_id([map()]) :: <<_::16, _::_*8>>
@doc """
Derives a keyset ID from a set of public keys.
## Parameters
- keys: A list of maps containing public keys.
## Returns
- A string representing the keyset ID.
"""
def derive_keyset_id(keys) do
sorted_keys = Enum.sort_by(keys, & &1.amount)
pubkeys_concat = Enum.map(sorted_keys, & &1.public_key) |> IO.iodata_to_binary()

"00" <>
(:crypto.hash(:sha256, pubkeys_concat) |> Base.encode16(case: :lower) |> binary_part(0, 14))
end
end
45 changes: 8 additions & 37 deletions lib/cashubrew/web/controllers/mint_controller.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
defmodule Cashubrew.Web.MintController do
use Cashubrew.Web, :controller
alias Cashubrew.Mint
alias Cashubrew.Nuts.Nut00.BlindedMessage
alias Cashubrew.Nuts.Nut00
alias Cashubrew.Nuts.Nut01
alias Cashubrew.Nuts.Nut06
alias Cashubrew.Web.{Keys, KeysetResponse}

def info(conn, _params) do
info = Nut06.Info.info()
Expand Down Expand Up @@ -35,23 +35,14 @@ defmodule Cashubrew.Web.MintController do
repo = Application.get_env(:cashubrew, :repo)
keysets = Mint.get_active_keysets(repo)

keysets_responses =
keysets_with_keys =
Enum.map(keysets, fn keyset ->
keys = Mint.get_keys_for_keyset(repo, keyset.id)

keys_list =
Enum.map(keys, fn key -> {key.amount, Base.encode16(key.public_key, case: :lower)} end)

%KeysetResponse{
id: keyset.id,
unit: keyset.unit,
keys: %Keys{pairs: keys_list}
}
%{id: keyset.id, unit: keyset.unit, keys: keys}
end)

response = %{
keysets: keysets_responses
}
response = Nut01.Serde.GetKeysResponse.from_keysets(keysets_with_keys)

json(conn, response)
end
Expand All @@ -63,20 +54,8 @@ defmodule Cashubrew.Web.MintController do
if keyset do
keys = Mint.get_keys_for_keyset(repo, keyset_id)

keys_list =
Enum.map(keys, fn key ->
{key.amount, Base.encode16(key.public_key, case: :lower)}
end)

keyset_response = %KeysetResponse{
id: keyset.id,
unit: keyset.unit,
keys: %Keys{pairs: keys_list}
}

response = %{
keysets: [keyset_response]
}
response =
Nut01.Serde.GetKeysResponse.from_keysets([%{id: keyset.id, unit: keyset.unit, keys: keys}])

json(conn, response)
else
Expand All @@ -93,20 +72,12 @@ defmodule Cashubrew.Web.MintController do
defp validate_blinded_messages(outputs) do
blinded_messages =
Enum.map(outputs, fn output ->
convert_to_blinded_message(output)
Nut00.BlindedMessage.from_map(output)
end)

{:ok, blinded_messages}
end

defp convert_to_blinded_message(output) do
%BlindedMessage{
amount: Map.fetch!(output, "amount"),
id: Map.fetch!(output, "id"),
B_: Map.fetch!(output, "B_")
}
end

def create_mint_quote(conn, %{"amount" => amount, "unit" => "sat"} = params) do
description = Map.get(params, "description")

Expand Down
32 changes: 0 additions & 32 deletions lib/cashubrew/web/keys.ex

This file was deleted.

7 changes: 0 additions & 7 deletions lib/cashubrew/web/keyset_response.ex

This file was deleted.

Loading

0 comments on commit eeda98f

Please sign in to comment.